Cases from a CSV or an XLSX — Testomat, TestRail, Qase, Zephyr or columns of your own
const 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';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'text/csv'}, body: 'ID,Title,Section Hierarchy,Priority,Steps (Step),Steps (Expected Result)\nC1042,Apply a 10% coupon at checkout,Checkout > Coupons,High,Enter AUTUMN10 and press Apply,The total drops by 10 %\n'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}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).
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Query Parameters
Section titled “Query Parameters ”Report only; nothing is written, not even the folders.
The tool the file came from. Absent, the header says; custom has no key.
Example
key,title,suitePath,priority,precondition,step,stepExpected,skipWhat 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.
Example
testrail-cases.csvThe file’s name — the default folder and the suite for rows with no path are named after it.
A folder at the top of the tree to import into, made if there is none.
A folder of the tree to import into.
Request Body required
Section titled “Request Body required ”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 %Responses
Section titled “ Responses ”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
The namespace the keys are matched and kept in — absent for a custom file, or one with no key column.
Where the file goes. id once the folder exists — absent on a dry run that would make it.
object
Rows of the file that say something.
object
Absent on a dry run for a case that would be created.
object
Absent on a dry run for a case that would be created.
object
Absent on a dry run for a case that would be created.
object
object
object
object
object
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 }}Example
{"progress":{"done":1,"total":1}}{"report":{"dryRun":false}}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
object
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
object
Example generated
{ "error": "example"}Over 10 MB
object
Example generated
{ "error": "example"}A body that is not text/csv (or text/plain) nor an XLSX
object
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.
object
The body of a 429 that is a quota and not a throttle: the sentence, and the figures behind it.
object
What was reached and the way out.
object
The ceiling. A write that would take used past it is refused with 429.
What the project holds now, counted by the same query that refuses a write.
limit - used, never below 0.
When the oldest counted run ages out of the rolling 24 hours and a slot frees on its own. null for the two quotas only an action frees (delete cases, decide queue items), and for a run quota with no runs in the window.
Example
{ "quota": { "kind": "runsPerDay" }}Headers
Section titled “Headers ”Seconds until the window rolls over on a throttle; on a quota, until quota.resetsAt — sent only for the run quota, which has one