Skip to content

Cases from a CSV or an XLSX — Testomat, TestRail, Qase, Zephyr or columns of your own

POST
/v1/test-cases/import
curl --request POST \
--url 'https://beta-api.plune.ai/v1/test-cases/import?source=testomat&columns=key%2Ctitle%2CsuitePath%2Cpriority%2Cprecondition%2Cstep%2CstepExpected%2Cskip&file=testrail-cases.csv' \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: text/csv' \
--data 'ID,Title,Section Hierarchy,Priority,Steps (Step),Steps (Expected Result)
C1042,Apply a 10% coupon at checkout,Checkout > Coupons,High,Enter AUTUMN10 and press Apply,The total drops by 10 %
'

The body is the file (#1235): RFC 4180, comma-separated (a header split by ; or a tab is read by that), a UTF-8 BOM dropped, up to 20 000 rows and 200 columns. Cells are text — a formula is not evaluated. Or an Excel workbook, sent as application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: its first visible sheet is read into the same header and rows — each cell as the file stores it (a formula’s last value, a number’s digits, a boolean as TRUE/FALSE), at most 100 MB of XML once unpacked — and goes the same way from there. Each column becomes a case field (columns, else the preset of source, else the field of its own name): a row with a title starts a case, a row with none that has a step or an expected result goes on with the case above it (one step per row, as TestRail and Zephyr write them). Steps pair with expected results by their numbers (1. …), else by position; a description in Testomat’s Markdown gives its ## Preconditions, ## Steps and ## Expected result. Tags join the title. Suite paths (A > B, /A/B, Qase’s suite ids) become folders, then the suite, under the target: folderId, else a folder at the top named folder, else after file — made if missing; a row with no path goes into a suite named after the file, and a node that holds cases and other nodes becomes a folder with its own cases in a suite of its name. A case the tool’s key already names (source’s namespace) is updated — title, texts, priority, steps, its suite — so the same file again adds nothing twice; any other is created as a draft with the key. Nothing is deleted; type and state never change. A row is refused, with its reason, when it has no title, a priority Plune has no place for, a path with an empty part, a key twice in the file, a key a case in the Bin holds, a key on a case that is not manual, or no step or expectation. Each write is its own: what was done before a refusal stays. ?dryRun=true gives the same report and writes nothing. The one refusal before any write: a file whose new cases would take the project past its case limit is answered 429, nothing written; the dry run reports the numbers in ceiling instead. With Accept: application/x-ndjson a write answers one JSON per line — {"progress":{"done","total"}} as cases are written, then {"report":…} (or {"error":…} if it stopped).

dryRun
boolean

Report only; nothing is written, not even the folders.

source
string
Allowed values: testomat testrail qase zephyr custom

The tool the file came from. Absent, the header says; custom has no key.

columns
string
Example
key,title,suitePath,priority,precondition,step,stepExpected,skip

What each column of the file becomes, comma-separated, in its order — one of skip, title, key, suitePath, suiteId, suiteParent, priority, description, precondition, step, stepExpected, expected, tags, isFlaky, estimate. title, key, suitePath, suiteId, suiteParent, priority and isFlaky come from one column at most; a title is required. isFlaky (Qase’s is_flaky column, #1246) only sets the flag: yes, true or 1 sets it; no, false, 0, an empty cell or any other word leaves the case’s flag as it is — a file has no base version to clear it by. estimate (TestRail’s Estimate, Zephyr’s Estimated Time, #1881) is the case’s time estimate: 30s, 5m, 1h 30m, a bare number is minutes; an empty cell leaves the case’s estimate as it is, and a cell that is not an estimate is a warning on its row — the case is imported without it. estimate comes from one column at most.

file
string
Example
testrail-cases.csv

The file’s name — the default folder and the suite for rows with no path are named after it.

folder
string

A folder at the top of the tree to import into, made if there is none.

folderId
string format: uuid

A folder of the tree to import into.

string
Example
ID,Title,Section Hierarchy,Priority,Steps (Step),Steps (Expected Result)
C1042,Apply a 10% coupon at checkout,Checkout > Coupons,High,Enter AUTUMN10 and press Apply,The total drops by 10 %

The report; refusals of single rows are inside it. With Accept: application/x-ndjson on a write, lines of progress and then the report.

What a CSV import did, row by row (#1235) — or, with dryRun, would do, nothing written. row is the row in the sheet, the header being row 1; a case starts on the row with its title. refused names each row that could not become a case and why; suites.created the folders and suites made on the way, by path from the target; warnings what was left out (a word in the tags column that is not a tag). ceiling is the project before the file and after its new cases, against its case limit: a dry run past the limit says so here, the write is answered 429.

object
dryRun
required
boolean
source
required
string
Allowed values: testomat testrail qase zephyr custom
keyKind

The namespace the keys are matched and kept in — absent for a custom file, or one with no key column.

string
Allowed values: playwright-id path-title allure-history cairn-stable eval-id qase testrail testomat zephyr
folder
required

Where the file goes. id once the folder exists — absent on a dry run that would make it.

object
id
string format: uuid
name
required
string
rows
required

Rows of the file that say something.

integer
created
required
Array<object>
object
id

Absent on a dry run for a case that would be created.

string format: uuid
title
required
string
row
required
integer
updated
required
Array<object>
object
id

Absent on a dry run for a case that would be created.

string format: uuid
title
required
string
row
required
integer
unchanged
required
Array<object>
object
id

Absent on a dry run for a case that would be created.

string format: uuid
title
required
string
row
required
integer
refused
required
Array<object>
object
title
required
string
row
required
integer
error
required
string
suites
required
object
created
required
Array<object>
object
id
string format: uuid
path
required
string
row
required
integer
warnings
required
Array<object>
object
row
required
integer
message
required
string
ceiling
required
object
now
required
integer
after
required
integer
limit
required
integer
Example
{
"dryRun": false,
"source": "testrail",
"keyKind": "testrail",
"folder": {
"id": "6f1c2b3a-0000-4000-8000-0000000000d1",
"name": "testrail-cases"
},
"rows": 1,
"created": [
{
"id": "6f1c2b3a-0000-4000-8000-0000000000d4",
"title": "Apply a 10% coupon at checkout",
"row": 2
}
],
"updated": [],
"unchanged": [],
"refused": [],
"suites": {
"created": [
{
"id": "6f1c2b3a-0000-4000-8000-0000000000d1",
"path": "testrail-cases",
"row": 2
},
{
"id": "6f1c2b3a-0000-4000-8000-0000000000d2",
"path": "testrail-cases > Checkout",
"row": 2
},
{
"id": "6f1c2b3a-0000-4000-8000-0000000000d3",
"path": "testrail-cases > Checkout > Coupons",
"row": 2
}
]
},
"warnings": [],
"ceiling": {
"now": 3,
"after": 4,
"limit": 20000
}
}

A file that is neither a CSV nor a workbook Plune reads, is empty or past a cap, or columns that do not fit it

Media type application/json
object
error
required
string
Example
{
"error": "no column becomes the title — every case needs one"
}

No / invalid token or session

folderId is not a folder of the caller’s project

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}

Over 10 MB

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}

A body that is not text/csv (or text/plain) nor an XLSX

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}

Two refusals share this status, and what to do about them is opposite. Rate-limited — the per-route throttle, or the per-minute request limit of the plan an API token counts by (#1442, #2092): Retry-After carries the seconds until the window rolls over, and waiting that long is the way out. Quota reached — a ceiling on what the project holds (runs in the last 24 hours, test cases, items waiting in the review queue): { error, quota }. A retry straight away frees nothing, so do not loop on it; the message says what makes room — deleting cases, working the queue down, an operator raising the limit — and the run quota, a rolling 24 hours, frees its oldest runs as they age out. Tell the two apart by the body: a 429 whose body has a quota object is a quota, one without it is a throttle. quota carries kind, limit, used, remaining and resetsAt: the figures behind the sentence, and for the run quota the instant its oldest run ages out (#1258). Retry-After is sent on a quota only where there is such an instant (the run quota: the whole seconds, at least 1, until resetsAt); the case, review-queue and project quotas free only by an action, have resetsAt: null, and send no Retry-After. GET /v1/quotas reads the same figures without a refusal.

Media type application/json
One of:
object
error
required
string
Example
{
"quota": {
"kind": "runsPerDay"
}
}
Retry-After
integer

Seconds until the window rolls over on a throttle; on a quota, until quota.resetsAt — sent only for the run quota, which has one