Dry run: what an import of one TestRail project would do
const url = 'https://beta-api.plune.ai/v1/projects/example/integrations/testrail/dry-run';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/dry-run \ --header 'Content-Type: application/json' \ --cookie plune_session=<plune_session> \ --data '{ "projectId": 1 }'Owner only. Starts a job (kind: testrail-dry-run, in GET /v1/jobs) that reads the instance the way the import does and writes nothing in the project: it counts the tree and the cases that would be created, updated, left alone, in conflict or skipped, and why. The counters are those the import after it gives, from the same code. Answers 202 with the job’s view; poll GET …/imports/{jobId}. One dry run or import at a time in a project (409). With history it reads the lists of runs and plans too (never the results): it counts the runs that would come, those left out (older than the plan of the account keeps, in the Bin, finished in Plune already), estimates the results from the counts of the lists and says which sub-statuses the import would add. An import of a TestRail project needs a finished dry run of that project first. Mapping: docs/features/BH-A1-testrail-bridge/mapping.md. 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": true, "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"}A dry run or an import of this project is running, or the saved key cannot be read on this server
object
Example
{ "error": "a dry run or an import of this project is running - wait for it, or cancel it"}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"}