Skip to content

Import one TestRail project

POST
/v1/projects/{id}/integrations/testrail/imports
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.

id
required
string

An id from GET /v1/projects

Media type application/json
object
projectId
required

The id of the project in TestRail, from GET …/projects.

integer
>= 1
customFields

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

Array<object>
<= 50 items
object
systemName
required

TestRail’s system_name as the dry run lists it.

string
action
required

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.

string
Allowed values: create map
pluneFieldId

With map only: the id of the Plune custom field.

string
history

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
mode
required
string
Allowed values: none months all
months

Only with mode: months. Default 12.

integer
>= 1 <= 120
Example
{
"projectId": 1
}

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": 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

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"
}

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

Media type application/json
object
error
required
string
Examples
Example dryRun

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

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"
}