Import one TestRail project
const url = 'https://beta-api.plune.ai/v1/projects/example/integrations/testrail/imports';const options = { method: 'POST', headers: { cookie: 'plune_session=<plune_session>', 'Content-Type': 'application/json' }, body: '{"projectId":1}'};
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/projects/example/integrations/testrail/imports \ --header 'Content-Type: application/json' \ --cookie plune_session=<plune_session> \ --data '{ "projectId": 1 }'Owner only, and only after a finished dry run of the same TestRail project, made on this connection (the same address) within the last 7 days (409 otherwise; 409 too when cases of another TestRail instance were imported into this project before). Starts a job (kind: testrail-import) that works in bounded steps on the queue: the lookups, the suites and sections, the tree (a folder for the project, one for each suite in suite mode 3, nested folders and suites for the sections), then the cases with their steps (both templates; shared steps expanded), priority, refs (a custom field) and labels (tags). A step records its cursor { phase, id, offset } and queues the next in one transaction, so a worker that dies goes on from there without a duplicate. A case is found again by the external key testrail / C<id>: a re-import updates, never duplicates. A case in the Bin is not brought back; a case changed in Plune since the import is not overwritten (a conflict when TestRail changed it too — see conflicts). Nothing is ever deleted. With history (BH-A3b) the same chain goes on after the cases: the runs (key testrail:run:<id>, kind manual, with the dates TestRail has) and a page of 250 results a step (source testrail, key testrail:result:<id>: a repeat adds nothing). A closed TestRail run is finished at its completed_on, an open one stays open; a run finished in Plune already, or in the Bin, is skipped and counted. Plans are not imported (a run inside one carries a Plan: label), milestones are a Release: label. Statuses: Passed, Blocked, Failed as they are; Retest and a status of the instance’s own are skipped with a sub-status of that name (added to the project, ten at most per status); an unknown status id is skipped. Untested makes no result. People (authors, assignees) and the attachments of runs and results are not carried. Attachments and users are not imported — the counters say what was left out. The estimate is read into the case’s estimate. Case fields the owner chose in customFields are carried into custom fields of cases (a value already in Plune is never overwritten, so a second import changes nothing and makes no second field). At most 100 requests a minute to the instance for the connection in the worker process; at a 429 the step stops, and the next one is started after the Retry-After (20 pauses without progress fail the import). An import over 200 000 records (800 000 with history) or 3 000 requests fails with a reason. Text of a case is cut to what a case holds. Only an owner can start, cancel, retry or resume it, here and in the jobs queue (403 project owner only for anyone else). A failure carries one of these codes: address_rejected - the address is not https, or does not resolve to a public address; unreachable - the instance did not answer in 30 seconds; credentials_rejected - TestRail answered 401 or 403: the e-mail and API key are not accepted (a revoked key included); rate_limited - TestRail asked Plune to wait: a short pause (30 seconds at most) is waited out up to three times in a call, a longer one ends the call at once; an import then goes on after the pause by itself, and fails after 20 pauses without progress; redirect_refused - the address answers with a redirect, which Plune never follows; not_json - the address answers, but not like TestRail; too_large - an answer past 20 MB; pagination_unsupported - the instance does not page its answers (limit and offset need TestRail 6.7): import a file instead; unexpected - a status the call did not expect: status carries it. The instance’s own text is never passed on. Session cookie only.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “Path Parameters ”An id from GET /v1/projects
Request Body required
Section titled “Request Body required ”object
The id of the project in TestRail, from GET …/projects.
An import only (#1930): the case fields of the instance to carry into Plune custom fields, chosen from the list the dry run gave (fields of the dry run; default none). Owner only. 400 for a systemName the dry run did not list, a field that is not supported (Not supported: <type>: multi-select, user, milestone, …), a repeated field, two fields mapped to one Plune field, a map target that is not a live custom field of cases or cannot hold the kind (text and date need a string field, integer a number field, dropdown a single-value list, checkbox a single-value list with an option “Yes”), or a name create would take that a field of another kind holds. A dry run takes none (400).
object
TestRail’s system_name as the dry run lists it.
create: a Plune custom field of cases with the same name as the TestRail field (found again, not made twice, by its name, when it exists and fits). map: an existing one.
With map only: the id of the Plune custom field.
Runs and results with their history (#1893). Left out, or none: the cases alone. months: the runs created in the last months (12 when left out; at most 120). all: every run the plan of the account keeps (Free keeps 30 days, so on Free the older runs are counted in runs_beyond_plan and not imported). An import with history stands on a dry run that asked for it too (409 otherwise).
object
Only with mode: months. Default 12.
Example
{ "projectId": 1}Responses
Section titled “ Responses ”The job, queued
A dry run or an import. Its id is the id of its job in the queue (GET /v1/jobs); this is the part the queue has no place for.
object
paused_rate_limit: TestRail asked Plune to wait (429) for longer than a step takes — the next step goes on after the pause. credentials_rejected: the key was refused, no retry was made; replace the key and press Retry on the job.
Where the cursor { phase, id, offset } is: the lookups (priorities, shared steps), the suites, the sections, the tree in Plune, the cases, with history the runs (the lists) and the results (a page of 250 a step), and for a sync the cases no full pass found (gone).
Every counter, 0 when it did not happen. A dry run’s are what the import after it does, from the same code. cases_seen is what the instance listed; cases_created, cases_updated, cases_unchanged and cases_conflict (changed in TestRail and in Plune since the import: left as they are, listed in conflicts) sum with the cases_skipped_* (bin: the case is in the Bin and stays there; deleted: TestRail marks it deleted; bdd: Plune has no Gherkin steps; ceiling: the project has no room; invalid: Plune refused the case, or its section was not listed) to cases_seen. nodes_created / nodes_existing: the folders and suites of the tree. The rest count what the mapping leaves out or makes up: steps_shared_expanded, steps_shared_missing, cases_placeholder_expected, cases_exploratory, cases_estimate (the TestRail estimate was read into the case’s estimate: 30s 2m is 2m 30s; written for a new case and, where the case has none, for one that exists) and cases_estimate_not_transferred (text that is not an estimate: left out), cases_type_not_transferred, cases_custom_fields_not_transferred (cases with a value in a field that was not chosen or cannot be carried), priorities_unknown; an import that chose fields adds cases_custom_values (values that came across), cases_custom_values_skipped (a value the Plune field cannot hold: an option it does not have, a date in a local spelling) and custom_fields_refused (a chosen field that could not be made or mapped: it is left out, the warnings say why), and counts fields_created / fields_existing for the fields it set up; cases_refs is how many carried refs (kept in a custom field); cases_same_title is how many repeat the title of an earlier case of the same TestRail section (the same folder): they are imported all, never merged. A sync adds cases_gone (listed in gone), cases_detached (of those, moved to detached), cases_reattached (found again), fields_created and fields_existing; a sync deletes nothing. History (an import that asked for it): runs_listed = runs_beyond_plan + runs_binned (in the Bin: not brought back) + runs_already_imported (finished in Plune) + runs_to_import; runs_imported is how many of those an import has finished (0 in a dry run), runs_finished the closed ones among them. results_estimated and results_untested come from the lists of runs (a dry run reads no results). The import reads results_seen: results_accepted (new), results_duplicate (the key and the content are there), results_conflict (the key is there with other content), results_no_case (the case is not in Plune: not imported, in the Bin), results_skipped_untested, results_skipped_invalid; results_status_unknown (a status the instance did not list: skipped, the id in the raw status), substatuses_added and substatus_overflow (past the ten a status takes: the result stays skipped without one, the word is in the raw status), dates_clamped (times cut to 2004 … now), dates_missing (a run or result came without a time: dated by the import), runs_skipped_invalid (a run listed without an id or a creation time), descriptions_truncated (a run description longer than 200 characters is kept cut to 200), results_elapsed_unparsed (not a time, or longer than 24 days), results_attachments_not_transferred. A run counts the newest result of each case; older ones stay as the case’s history.
object
The cases left alone because both sides changed them — at most 100 of them. A sync also says when each side last changed the case (the case stays as it is in Plune).
object
A sync (or its dry run): what it was asked. null for an import.
object
A sync, or its dry run: the cases it made (created) or rewrote (updated, with the fields of the case the import owns that changed) — at most 100. caseId is absent for a case a dry run would make.
object
What history was asked for, and the dates it reaches back to; null for a sync, and for an import that asked for none. since is the oldest creation date of a run it brings (null: no limit; set once the first step of the runs is done), planDays the days of runs the plan of the account keeps (null: all).
object
The case fields of the instance (#1930): read by get_case_fields (one request) in a dry run, and in an import that was asked for fields; null otherwise, and for a sync. Not the fields the mapping reads itself, the system fields, the inactive, nor those that belong to other TestRail projects. cases is the number of cases with a value in it (an unticked checkbox, a dropdown on “None” and a zero are no value), complete when the cases are read. Every member sees the list; only the owner chooses (customFields of the import).
object
text, dropdown, checkbox, date, integer; or the TestRail type that is not carried: multi-select, user, milestone, steps, scenario.
Not supported: <type> when supported is false.
A dropdown’s options.
An import: what it was asked; pluneFieldId is the Plune field it used (null until the lookups made it).
object
A sync: the cases a full pass of the TestRail project did not find — at most 100. Nothing is deleted. detached: the case was moved to the detached state (only an active case may be; a draft stays a draft and is only listed).
object
At most 20 sentences about what the instance did not list or could not give.
When the job first reached each phase (the keys are those of phase): lookups is startedAt, done is finishedAt, a phase the job skipped is absent. A phase lasts until the next stamp. Jobs started before this field have only lookups and the stamps of the steps after it.
object
The job in the queue: its state, how many cases it has seen of how many it knows of so far (null until the cases begin), and why it stopped.
object
object
Example
{ "id": "b1f0c6e2-6c1e-4b0e-9a49-3f6f1c1a9d11", "dryRun": false, "status": "running", "trProjectId": 1, "trProjectName": "Project Alpha", "phase": "lookups", "counters": { "cases_seen": 4, "cases_created": 3, "cases_updated": 0, "cases_unchanged": 0, "cases_conflict": 0, "cases_skipped_bin": 0, "cases_skipped_deleted": 1, "cases_skipped_bdd": 0, "cases_skipped_ceiling": 0, "cases_skipped_invalid": 0, "nodes_created": 6, "nodes_existing": 0, "steps_shared_expanded": 1, "steps_shared_missing": 0, "cases_placeholder_expected": 0, "cases_exploratory": 1, "cases_estimate": 1, "cases_estimate_not_transferred": 0, "cases_refs": 1, "cases_same_title": 0, "cases_type_not_transferred": 3, "cases_custom_fields_not_transferred": 0, "priorities_unknown": 0, "cases_gone": 0, "cases_detached": 0, "cases_reattached": 0, "fields_created": 0, "fields_existing": 0, "cases_custom_values": 0, "cases_custom_values_skipped": 0, "custom_fields_refused": 0, "runs_listed": 0, "runs_beyond_plan": 0, "runs_binned": 0, "runs_already_imported": 0, "runs_to_import": 0, "runs_imported": 0, "runs_finished": 0, "results_estimated": 0, "results_untested": 0, "results_seen": 0, "results_accepted": 0, "results_duplicate": 0, "results_conflict": 0, "results_no_case": 0, "results_skipped_untested": 0, "results_skipped_invalid": 0, "results_status_unknown": 0, "substatuses_added": 0, "substatus_overflow": 0, "dates_clamped": 0, "results_elapsed_unparsed": 0, "results_attachments_not_transferred": 0, "runs_skipped_invalid": 0, "dates_missing": 0, "descriptions_truncated": 0 }, "conflicts": [], "warnings": [], "sync": null, "changes": [], "gone": [], "history": null, "fields": [ { "systemName": "custom_ai_type", "label": "AI Type", "type": "dropdown", "supported": true, "reason": null, "cases": 2, "options": [ "RAG", "ML", "LLM" ], "choice": null }, { "systemName": "custom_case_is_automated", "label": "Is Automated", "type": "checkbox", "supported": true, "reason": null, "cases": 1, "options": [], "choice": null }, { "systemName": "custom_platforms", "label": "Platforms", "type": "multi-select", "supported": false, "reason": "Not supported: multi-select", "cases": 1, "options": [], "choice": null } ], "phaseAt": { "lookups": "2026-09-16T09:12:04.151Z", "suites": "2026-09-16T09:12:04.151Z", "sections": "2026-09-16T09:12:04.151Z", "nodes": "2026-09-16T09:12:04.151Z", "cases": "2026-09-16T09:12:04.151Z", "done": "2026-09-16T09:12:04.151Z" }, "startedAt": "2026-09-16T09:12:04.151Z", "finishedAt": null, "job": { "state": "in_progress", "progress": null, "reason": null }}Invalid body; the message names the field and never a value
object
Example generated
{ "error": "example"}No / invalid session — a CLI token is refused here
You are in the project, but not as its owner
object
Example
{ "error": "project owner only"}No such project among yours, no TestRail connected, or no such TestRail project for this key
object
Example generated
{ "error": "example"}No finished, recent dry run of this TestRail project on this connection, cases of another instance already in this project, a dry run or an import of this project is running, or the saved key cannot be read on this server
object
Examples
No dry run yet
{ "error": "run a dry run of this TestRail project first, on this connection and within the last 7 days - it shows what an import would do"}The instance said no at the start: failure is the code
object
Example generated
{ "error": "example"}Rate-limited: the per-route throttle, or the per-minute request limit of the plan an API token counts by (#1442; the body names the plan, #2092). Retry-After carries the seconds until the window rolls over.
object
Example generated
{ "error": "example"}Headers
Section titled “Headers ”Seconds until the window rolls over
The server has no key to keep TestRail keys under
object
Example
{ "error": "the TestRail bridge is not configured on this server - contact your operator"}