When migrating from another test management tool, or when your test cases already live in a spreadsheet, the Testmo REST API makes it straightforward to import your entire test library programmatically, including any attached files. This article walks through the APIs involved and demonstrates how to use them together, using a Python script as a concrete example.
APIs for importing test cases
Three API namespaces work together for a full import:
-
Cases API:
POST /api/v1/projects/{project_id}/casescreates up to 100 test cases per request. The only required field per case is name; all other fields are optional. Where values are not provided, project defaults are used. - Attachments API:
POST /api/v1/cases/{case_id}/attachmentsuploads up to 20 files to a specific test case in a single request. Attachments must be added after the cases are created, since you need the case IDs returned by the Cases API. -
Folders API:
POST /api/v1/projects/{project_id}/folderscreates up to 100 folders per request. If you want to organize imported cases into a folder structure, create the folders first and then pass the resulting folder IDs when creating the cases. If no folder is specified, Testmo will automatically create one.
Each of these endpoints supports the full range of CRUD operations, so they can be used for ongoing maintenance as well as initial imports.
Example script
Testmo provides a Python example script to demonstrate how these APIs work together. The script reads test cases from a CSV file, creates the cases in Testmo, and then uploads their attachments. It is intended as a starting point and working illustration, not as a prescribed or only way to use the APIs. Your own implementation can use any language or approach that suits your environment.
You can download the script, sample CSV, and sample attachments from the Testmo API examples repository.
Setup
The script has two mandatory arguments and several optional ones:
| Argument | Required | Description |
|---|---|---|
| CSV file path | Yes | Path to your CSV file containing test case data |
| Project ID | Yes | The ID of the Testmo project to import into |
| --base-url | No | Your Testmo instance URL |
| --token | No | Your Testmo API token |
| --attachment-directory | No | Directory containing attachment files (default: attachments/ next to the script) |
| --dry-run | No | Parses the CSV and validates formatting without making any API requests |
The script reads TESTMO_BASE_URL and TESTMO_TOKEN from environment variables, using the same variable names as the Testmo CLI. If you have already configured the CLI, those variables will work automatically. You can also pass the values as arguments.
CSV format
The example CSV includes columns for the most commonly used test case fields:
| Column | Maps to |
|---|---|
| name | Test case name (required) |
| description | custom_description custom field |
| expected_results | custom_expected custom field |
| state | state_id (by name, mapped to integer ID) |
| estimate | estimate (in seconds) |
| priority | custom_priority (by name, mapped to integer ID) |
| tags | tags (comma-separated list) |
| attachments | Filenames to upload after case creation (comma-separated) |
Fields like state and priority use human-readable names in the CSV; the script maps them to the integer IDs that the API requires. The default state IDs in Testmo are:
| ID | State |
|---|---|
| 1 | Draft |
| 2 | Under Review |
| 3 | Rejected |
| 4 | Active |
| 5 | Retired |
The default priority IDs are:
| ID | Priority |
|---|---|
| 1 | High |
| 2 | Normal |
| 3 | Low |
If you have added or changed states and priorities in your project's administration settings, update the mappings in the script to match your configuration. A quick way to discover your current IDs is to create a test case manually, change a field, and check the ID returned by a GET /api/v1/projects/{project_id}/cases request.
How the script works
The script is organized into four sections.
- Helper functions
A set of utility functions ensures that values are in the correct formats before being sent to the API. For example, the estimate value is converted to seconds, tag strings are split into a list, and custom_ prefixes are added to custom field names.
- CSV loading and transformation
The script reads each row of the CSV and builds the JSON payload for the Cases API. Attachment filenames are kept separate at this stage because attachments must be uploaded in a dedicated API call after the cases are created.
- API calls
Three main API calls run in sequence:
- Authenticate: verify credentials and confirm the instance URL.
-
Create test cases: send up to 100 cases per request to
POST /api/v1/projects/{project_id}/cases. The response includes the id for each created case, which is used in the next step. -
Upload attachments: for each case, send files to
POST /api/v1/cases/{case_id}/attachments. The script automatically handles cases with more than 20 attachments by splitting them into multiple requests (the API accepts a maximum of 20 files per request). It also silently skips any filenames listed in the CSV that are not found in the attachments directory.
- CLI and main function
The argument parser handles the CLI interface. The main function calls the above sections in order: parse CSV, create cases, upload attachments.
Running the script
# Set environment variables (or use --base-url and --token arguments)
export TESTMO_BASE_URL=https://your-instance.testmo.net
export TESTMO_TOKEN=your-api-token
# Dry run to validate CSV without making API calls
python3 import_cases.py cases.csv 1 --dry-run
# Full import
python3 import_cases.py cases.csv 1Extending the example
The script covers the most common fields, but the Cases and Folders APIs support more. Here are a few patterns you can build on.
Importing into a folder structure
If your source data has a folder hierarchy, create the folders before creating the cases:
POST /api/v1/projects/1/folders
{
"folders": [
{ "name": "Authentication" },
{ "name": "Checkout" }
]
} The response returns the id of each created folder. Pass the relevant folder_id when creating each case:
POST /api/v1/projects/1/cases
{
"cases": [
{
"name": "Login with valid credentials",
"folder_id": 1101
}
]
}Importing test steps
If your cases use the steps template, include a custom_steps array. Each step object takes text1 for the step description and text3 for the expected result:
POST /api/v1/projects/1/cases
{
"cases": [
{
"name": "Place an order",
"template_id": 2,
"custom_steps": [
{ "text1": "<p>Add item to cart</p>", "text3": "<p>Item appears in cart</p>" },
{ "text1": "<p>Proceed to checkout</p>", "text3": "<p>Order summary is shown</p>" }
]
}
]
}Linking issues during import
If your source data includes issue tracker references, they can be linked at creation time:
POST /api/v1/projects/1/cases
{
"cases": [
{
"name": "Verify password reset flow",
"issues": [
{
"display_id": "PROJ-42",
"integration_id": 3
}
]
}
]
} For GitHub and GitLab integrations, also include connection_project_id.
Bulk updating after import
After importing, you can use PATCH /api/v1/projects/{project_id}/cases to update fields across up to 100 cases at once, for example, to move a batch of cases to a different folder or change their state.