Skip to content

Dry run of a sync: what it would change

POST
/v1/projects/{id}/integrations/testrail/sync/dry-run
curl --request POST \
--url https://beta-api.plune.ai/v1/projects/example/integrations/testrail/sync/dry-run \
--header 'Content-Type: application/json' \
--cookie plune_session=<plune_session> \
--data '{ "entities": [ "cases", "suites" ], "which": "all" }'

Owner only. The same job as POST …/sync with the writes off (kind: testrail-dry-run): it reads TestRail and counts what a sync would create, update, leave alone, list as a conflict and find gone — the counters, and the first 100 changes, conflicts and gone, are those the sync after it gives, from the same code. Nothing is written in the project, nothing to TestRail. The same refusals as the sync. 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.

id
required
string

An id from GET /v1/projects

Media type application/json
object
trProjectId

The TestRail project to look at, one that was imported on this connection. May be left out when exactly one was.

integer
>= 1
entities
required

What to look at. Each must be set to testrail in the modes (409 otherwise). cases also reads the tree; suites alone is the suite level of the tree; fields makes the references field.

Array<string>
>= 1 items unique items
Allowed values: cases suites fields
which
required

all: update what was imported where TestRail changed it (and Plune did not). new: add what is missing and write over nothing; a case both sides changed is only listed.

string
Allowed values: all new
Example
{
"entities": [
"cases",
"suites"
],
"which": "all"
}

The job, queued

Media type application/json

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
id
required
string
dryRun
required
boolean
status
required

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.

string
Allowed values: running paused_rate_limit credentials_rejected failed cancelled done
trProjectId
required
integer
trProjectName
required
string
phase
required

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).

string
Allowed values: lookups suites sections nodes cases runs results gone done
counters
required

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
key
additional properties
integer
conflicts
required

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).

Array<object>
<= 100 items
object
trId
required
integer
caseId
required
string
title
required
string
pluneChangedAt
string format: date-time
trChangedAt
string format: date-time
nullable
sync
required

A sync (or its dry run): what it was asked. null for an import.

object
which
string
Allowed values: all new
entities
Array<string>
changes
required

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.

Array<object>
<= 100 items
object
trId
required
integer
caseId
string
title
required
string
outcome
required
string
Allowed values: created updated
fields
required
Array<string>
history
required

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
mode
string
Allowed values: none months all
months
integer
nullable
since
string format: date-time
nullable
planDays
integer
nullable
fields
required

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).

Array<object>
nullable
object
systemName
required
string
label
required
string
type
required

text, dropdown, checkbox, date, integer; or the TestRail type that is not carried: multi-select, user, milestone, steps, scenario.

string
supported
required
boolean
reason
required

Not supported: <type> when supported is false.

string
nullable
cases
required
integer
options
required

A dropdown’s options.

Array<string>
choice
required

An import: what it was asked; pluneFieldId is the Plune field it used (null until the lookups made it).

object
action
string
Allowed values: create map
pluneFieldId
string
nullable
gone
required

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).

Array<object>
<= 100 items
object
trId
required
integer
caseId
required
string
title
required
string
detached
required
boolean
warnings
required

At most 20 sentences about what the instance did not list or could not give.

Array<string>
phaseAt
required

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
key
additional properties
string format: date-time
startedAt
required
string format: date-time
finishedAt
required
string format: date-time
nullable
job
required

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
state
required
string
progress
required
object
done
integer
total
integer
reason
required
string
nullable
Example
{
"id": "b1f0c6e2-6c1e-4b0e-9a49-3f6f1c1a9d11",
"dryRun": true,
"status": "running",
"trProjectId": 1,
"trProjectName": "Project Alpha",
"phase": "lookups",
"counters": {
"cases_seen": 5,
"cases_created": 1,
"cases_updated": 1,
"cases_unchanged": 1,
"cases_conflict": 1,
"cases_skipped_bin": 0,
"cases_skipped_deleted": 0,
"cases_skipped_bdd": 0,
"cases_skipped_ceiling": 0,
"cases_skipped_invalid": 0,
"nodes_created": 6,
"nodes_existing": 6,
"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": 1,
"cases_detached": 1,
"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": {
"which": "all",
"entities": [
"cases",
"suites"
]
},
"changes": [],
"gone": [],
"history": null,
"fields": 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, or several TestRail projects were imported and trProjectId is missing

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

No / invalid session — a CLI token is refused here

You are in the project, but not as its owner

Media type application/json
object
error
required
string
Example
{
"error": "project owner only"
}

No such project among yours, no TestRail connected, or no such TestRail project for this key

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

An entity asked for is off, no TestRail project was imported on this connection, a dry run, an import or a sync of this project is running, or the saved key cannot be read on this server

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

The instance said no at the start: failure is the code

Media type application/json
object
error
required
string
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.

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

Seconds until the window rolls over

The server has no key to keep TestRail keys under

Media type application/json
object
error
required
string
Example
{
"error": "the TestRail bridge is not configured on this server - contact your operator"
}