Programmatic access to QualityGraph — manage user journeys, test cases, test runs, and more via the REST API.
Base URL: https://qualitygraph.harnessqa.dev/api
All API requests require authentication via an API token. Tokens are created from the Settings page in the dashboard.
Include your token in the Authorization header:
curl -H "Authorization: Bearer qg_your_token_here" \
https://qualitygraph.harnessqa.dev/api/tms/journeys?orgId=YOUR_ORG_IDSecurity: API tokens grant access to all resources within the organization they were created for. Keep them secret — do not commit tokens to version control. Use environment variables or secret managers in CI/CD.
Everything the dashboard does, a token can do.
A token acts as the user who created it, inside one organization: test cases, journeys, runs, releases, runbooks, repositories, coverage, health, trends, flaky tests, pull-request risk and test gaps, quality gates, members and invitations, result ingestion and CLI uploads. Requests for another organization answer 403; the platform admin area is session-only. Revoking the token ends its access immediately.
Create and manage API tokens. Tokens are scoped to an organization and can optionally expire.
Navigate to Settings in the dashboard and click Create Token. You'll be shown the full token once — copy it immediately.
All API tokens are prefixed with qg_ followed by 64 hex characters:
qg_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
Store your token as an environment variable and reference it in scripts:
# GitHub Actions example
env:
QG_TOKEN: ${{ secrets.QUALITYGRAPH_TOKEN }}
steps:
- name: Create user journey
run: |
curl -X POST "https://qualitygraph.harnessqa.dev/api/tms/journeys" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"orgId":"YOUR_ORG_ID","title":"Checkout Flow","steps":[]}'All responses are wrapped in a standard envelope:
{
"data": { ... },
"meta": {
"timestamp": "2026-09-05T12:00:00.000Z"
}
}List endpoints return arrays in data. Single-resource endpoints return objects. Error responses follow the same envelope with an error field.
User journeys represent critical user flows in your application. Each journey has a sequence of steps (action + expected outcome) and can be linked to test cases to track coverage.
/tms/journeys?orgId=:orgIdList user journeys. Filters: folderId, includeSubfolders=true (with folderId), unfiled=true
/tms/journeys/:idGet a single journey with linked test cases
/tms/journeysCreate a new user journey; folderId files it into a folder
/tms/journeys/:idUpdate a user journey; folderId moves it (null = unfiled)
/tms/journeys/:idDelete a user journey
/tms/journeys/:id/casesLink test cases to a journey
/tms/journeys/:id/cases/:caseIdUnlink a test case from a journey
/tms/journeys/export?orgId=:orgIdDownload journeys as a file. format=json (default) or csv; folderId (+ includeSubfolders=true), unfiled=true, ids=a,b,c, status, priority, search narrow it
/tms/journeys/importImport journeys from JSON or CSV: { orgId, content, format?, targetFolderId?, onDuplicate?: skip|update|create, dryRun? }
Journeys live in folders that nest to any depth through parentId. Moving a folder into its own subtree is refused. Deleting a folder keeps its journeys (they become unfiled) and lifts its subfolders one level.
/tms/journey-folders?orgId=:orgIdList folders (flat, each with parentId, childrenCount and journeyCount)
/tms/journey-foldersCreate: { orgId, title, description?, parentId? }
/tms/journey-folders/:idRename ({ title }) or move ({ parentId } — null for the top level)
/tms/journey-folders/:idDelete a folder; journeys inside become unfiled
# A folder inside another, then a journey filed into it
curl -X POST "https://qualitygraph.harnessqa.dev/api/tms/journey-folders" \
-H "Authorization: Bearer $QG_TOKEN" -H "Content-Type: application/json" \
-d '{ "orgId": "YOUR_ORG_ID", "title": "Checkout", "parentId": "SHOPPING_FOLDER_ID" }'
curl -X PATCH "https://qualitygraph.harnessqa.dev/api/tms/journeys/JOURNEY_ID" \
-H "Authorization: Bearer $QG_TOKEN" -H "Content-Type: application/json" \
-d '{ "folderId": "CHECKOUT_FOLDER_ID" }'
# Everything under Shopping, subfolders included
curl -H "Authorization: Bearer $QG_TOKEN" \
"https://qualitygraph.harnessqa.dev/api/tms/journeys?orgId=YOUR_ORG_ID&folderId=SHOPPING_FOLDER_ID&includeSubfolders=true"curl -H "Authorization: Bearer $QG_TOKEN" \
"https://qualitygraph.harnessqa.dev/api/tms/journeys?orgId=YOUR_ORG_ID"curl -X POST "https://qualitygraph.harnessqa.dev/api/tms/journeys" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orgId": "YOUR_ORG_ID",
"title": "User Checkout Flow",
"description": "Complete purchase flow from cart to confirmation",
"priority": "HIGH",
"status": "ACTIVE",
"tags": ["checkout", "payment", "critical"],
"steps": [
{"order": 1, "action": "Add item to cart", "expectedOutcome": "Item appears in cart with correct price"},
{"order": 2, "action": "Proceed to checkout", "expectedOutcome": "Checkout form displays with cart summary"},
{"order": 3, "action": "Enter payment details", "expectedOutcome": "Payment form validates input"},
{"order": 4, "action": "Submit order", "expectedOutcome": "Order confirmation page with order ID"}
]
}'{
"data": {
"id": "a1b2c3d4-...",
"title": "User Checkout Flow",
"description": "Complete purchase flow from cart to confirmation",
"priority": "HIGH",
"status": "ACTIVE",
"tags": ["checkout", "payment", "critical"],
"steps": [
{"order": 1, "action": "Add item to cart", "expectedOutcome": "Item appears in cart with correct price"},
{"order": 2, "action": "Proceed to checkout", "expectedOutcome": "Checkout form displays with cart summary"},
{"order": 3, "action": "Enter payment details", "expectedOutcome": "Payment form validates input"},
{"order": 4, "action": "Submit order", "expectedOutcome": "Order confirmation page with order ID"}
],
"orgId": "YOUR_ORG_ID",
"createdAt": "2026-09-05T12:00:00.000Z",
"updatedAt": "2026-09-05T12:00:00.000Z"
}
}curl -X PATCH "https://qualitygraph.harnessqa.dev/api/tms/journeys/JOURNEY_ID" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"priority": "CRITICAL", "status": "ACTIVE"}'curl -X POST "https://qualitygraph.harnessqa.dev/api/tms/journeys/JOURNEY_ID/cases" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"testCaseIds": ["case-id-1", "case-id-2"]}'An export carries folders as paths (Shopping / Checkout) and linked test cases by name, never by id, so it can be imported into any organization. Import creates missing folders, links test cases whose names match, and matches duplicates by title within the same folder. dryRun: true validates the file and reports what would happen without writing anything.
# JSON export of one folder and everything under it
curl -o journeys.json -H "Authorization: Bearer $QG_TOKEN" \
"https://qualitygraph.harnessqa.dev/api/tms/journeys/export?orgId=YOUR_ORG_ID&folderId=SHOPPING_FOLDER_ID&includeSubfolders=true"
# CSV export: Folder,Title,Description,Steps,Priority,Status,Tags,Test Cases
curl -o journeys.csv -H "Authorization: Bearer $QG_TOKEN" \
"https://qualitygraph.harnessqa.dev/api/tms/journeys/export?orgId=YOUR_ORG_ID&format=csv"
# Import under a folder; the file contents travel in the JSON body
curl -X POST "https://qualitygraph.harnessqa.dev/api/tms/journeys/import" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg c "$(cat journeys.csv)" '{orgId: "YOUR_ORG_ID", content: $c, targetFolderId: "IMPORTED_FOLDER_ID", onDuplicate: "skip"}')"
# → { "format": "csv", "total": 12, "created": 10, "updated": 0, "skipped": 2, "foldersCreated": 3, "linked": 7, "errors": [], "warnings": [] }| Field | Type | Description |
|---|---|---|
| orgId | string (UUID) | Organization ID (required) |
| title | string | Journey name (required) |
| description | string | Detailed description |
| steps | array | Steps with order, action, expectedOutcome |
| priority | string | LOW, MEDIUM, HIGH, or CRITICAL |
| status | string | ACTIVE, DRAFT, or DEPRECATED |
| tags | string[] | Arbitrary tags for filtering |
Test suites are the folders of the Test Cases tab. They nest to any depth via the parentId field; a suite cannot be moved into its own subtree. Deleting a suite keeps its cases (they become unfiled) and lifts its subfolders one level.
/tms/suites?orgId=:orgIdList suites (flat, each with parentId, childrenCount and testCaseCount)
/tms/suitesCreate: { orgId, title, description?, parentId? }
/tms/suites/:idRename ({ title }) or move ({ parentId } — null for the top level)
/tms/suites/:idDelete a suite; cases inside become unfiled
curl -X POST "https://qualitygraph.harnessqa.dev/api/tms/suites" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orgId": "YOUR_ORG_ID",
"title": "Authentication Tests",
"description": "All test cases related to user authentication"
}'Individual test cases with steps, preconditions, and metadata. Can be linked to suites and user journeys.
/tms/cases?orgId=:orgIdList test cases. Filters: suiteId, includeSubfolders=true (with suiteId), unfiled=true, status, priority, search; page, limit (max 100)
/tms/cases/:idGet a test case with execution history
/tms/casesCreate a test case
/tms/cases/bulkBulk create test cases
/tms/cases/:idUpdate a test case; suiteId moves it between folders (null = unfiled)
/tms/cases/:idDelete a test case
/tms/cases/bulk-deleteDelete several test cases: { ids: [...] }
/tms/cases/export?orgId=:orgIdDownload test cases as a file. format=json (default) or csv; folderId (+ includeSubfolders=true), unfiled=true, ids=a,b,c, status, priority, search narrow it
/tms/cases/importImport test cases from JSON or CSV: { orgId, content, format?, targetFolderId?, onDuplicate?: skip|update|create, dryRun? }
JSON exports are lossless and versioned (format: "qualitygraph-test-cases", version: 1). CSV exports have the columns Folder, Name, Description, Preconditions, Steps, Priority, Type, Test Type, Status, Tags, Estimated Minutes, Journey; steps are written one per line as 1. action => expected and tags are separated by semicolons. On import, headers from other tools are understood as well (Section, Title, Expected Result, Labels, Automation Type, Estimate…), folder paths are created undertargetFolderId, and a case whose name already exists in the same folder is skipped, updated or duplicated according to onDuplicate. Rows that cannot be imported are reported with their line number; the rest still go in. Up to 5,000 rows per call.
# Export the cases you can see in a folder, as a spreadsheet
curl -o cases.csv -H "Authorization: Bearer $QG_TOKEN" \
"https://qualitygraph.harnessqa.dev/api/tms/cases/export?orgId=YOUR_ORG_ID&format=csv&folderId=SUITE_ID&includeSubfolders=true"
# Preview an import without writing anything
curl -X POST "https://qualitygraph.harnessqa.dev/api/tms/cases/import" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg c "$(cat cases.csv)" '{orgId: "YOUR_ORG_ID", content: $c, dryRun: true}')"
# Then import for real, updating cases that already exist
curl -X POST "https://qualitygraph.harnessqa.dev/api/tms/cases/import" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg c "$(cat cases.csv)" '{orgId: "YOUR_ORG_ID", content: $c, onDuplicate: "update"}')"
# → { "format": "csv", "total": 40, "created": 35, "updated": 5, "skipped": 0, "foldersCreated": 4, "linked": 12,
# "errors": [{ "row": 17, "message": "Name is required" }], "warnings": [] }curl -X POST "https://qualitygraph.harnessqa.dev/api/tms/cases" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orgId": "YOUR_ORG_ID",
"name": "Login with valid credentials",
"description": "Verify user can login with correct email and password",
"priority": "HIGH",
"caseType": "MANUAL",
"suiteId": "SUITE_ID",
"journeyId": "JOURNEY_ID",
"steps": [
{"stepNumber": 1, "action": "Navigate to /login", "expected": "Login form displayed"},
{"stepNumber": 2, "action": "Enter valid email and password", "expected": "Fields populated"},
{"stepNumber": 3, "action": "Click Sign In", "expected": "Redirected to dashboard"}
],
"tags": ["auth", "login", "smoke"]
}'curl -X POST "https://qualitygraph.harnessqa.dev/api/tms/cases/bulk" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"cases": [
{"orgId": "YOUR_ORG_ID", "name": "Login success", "priority": "HIGH"},
{"orgId": "YOUR_ORG_ID", "name": "Login invalid password", "priority": "MEDIUM"},
{"orgId": "YOUR_ORG_ID", "name": "Login rate limit", "priority": "LOW"}
]
}'| Param | Type | Description |
|---|---|---|
| orgId | string | Filter by organization |
| suiteId | string | Filter by test suite |
| status | string | ACTIVE, DRAFT, DEPRECATED |
| priority | string | LOW, MEDIUM, HIGH, CRITICAL |
| search | string | Search by name |
| page | number | Page number (default: 1) |
| limit | number | Items per page (default: 50) |
A test run is an execution session — a collection of test case results. Create a run, then submit results for each test case.
/tms/runs?orgId=:orgIdList test runs
/tms/runs/:idGet a test run with all results
/tms/runsCreate a test run
/tms/runs/:idUpdate a test run (status, etc.)
/tms/runs/:id/resultsSubmit test execution results
/tms/runs/:idDelete a test run
# 1. Create a test run
curl -X POST "https://qualitygraph.harnessqa.dev/api/tms/runs" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orgId": "YOUR_ORG_ID",
"title": "Nightly Regression - 2026-09-05",
"environment": "staging"
}'
# Response: {"data": {"id": "RUN_ID", ...}}
# 2. Submit results
curl -X POST "https://qualitygraph.harnessqa.dev/api/tms/runs/RUN_ID/results" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"results": [
{"testCaseId": "CASE_1", "status": "PASSED", "duration": 1.2},
{"testCaseId": "CASE_2", "status": "FAILED", "duration": 3.5, "notes": "Timeout on payment API"},
{"testCaseId": "CASE_3", "status": "SKIPPED"}
]
}'
# 3. Complete the run
curl -X PATCH "https://qualitygraph.harnessqa.dev/api/tms/runs/RUN_ID" \
-H "Authorization: Bearer $QG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "COMPLETED"}'Group test runs into releases and keep operational procedures next to them. Descriptions, notes and step instructions accept Markdown.
/tms/releases?orgId=:orgIdList releases with run roll-ups (status, tag, search filters)
/tms/releasesCreate a release: { orgId, name, version?, description?, targetDate?, notes?, tags? }
/tms/releases/:idRelease with its test runs and pass/fail totals
/tms/releases/:idUpdate fields or status (PLANNED, IN_PROGRESS, RELEASED, CANCELLED)
/tms/releases/:idDelete a release; its runs are kept
/tms/releases/:id/runsAttach runs: { runIds: [...] }
/tms/releases/:id/runs/:runIdDetach a run
/tms/runbooks?orgId=:orgIdList runbooks (category, status, tag, search filters)
/tms/runbooksCreate: { orgId, title, category, description?, steps: [{ order, title, instruction?, command? }], tags? }
/tms/runbooks/:idRunbook with steps
/tms/runbooks/:idUpdate fields, steps or status
/tms/runbooks/:idDelete a runbook
curl -X POST -H "Authorization: Bearer $QG_TOKEN" -H "Content-Type: application/json" \
"https://qualitygraph.harnessqa.dev/api/tms/releases" \
-d '{"orgId":"YOUR_ORG_ID","name":"2026.10","version":"v2026.10.0","notes":"## Checklist\n- [ ] smoke test"}'Organizations are the top-level scope for all resources. Your API token is bound to one organization.
/organizationsList organizations you belong to
/organizations/:idGet organization details
/organizations/:id/dashboardDashboard summary with KPIs
/organizations/:id/repositoriesList repositories
/organizations/:id/coverage-matrixCoverage matrix across repos
/organizations/:id/analyticsOrganization-level analytics
curl -H "Authorization: Bearer $QG_TOKEN" \
"https://qualitygraph.harnessqa.dev/api/organizations"Roles are owner, admin and member. Owners and admins manage members; only owners hand out the owner role. An organization always keeps at least one owner.
/organizations/:id/membersMembers with roles, plus your own permissions and whether email delivery is configured
/organizations/:id/members/:userIdChange a role: { role }
/organizations/:id/members/:userIdRemove a member (or leave, when it is you)
/organizations/:id/members/:userId/password-resetEmail that member a password reset link
/organizations/:id/invitationsPending invitations
/organizations/:id/invitationsInvite by email: { email, role } → invitation, inviteUrl, emailSent
/organizations/:id/invitations/:invitationId/resendSend a fresh link (the previous one is superseded)
/organizations/:id/invitations/:invitationIdRevoke an invitation
/invitations/:tokenPublic: what an invite link points to and its status
/invitations/:token/acceptAccept as the signed-in user
/repositories?orgId=:orgIdList repositories
/repositories/:idRepository details: branches, latest coverage per layer, latest health score, and stats (PR counts with high-risk open ones, commits, last commit, last coverage, test runs, last run)
/repositories/:id/healthHealth score and components
/repositories/:id/coverageLatest coverage per test layer; provider tells whether it was measured (LCOV, COBERTURA, JACOCO, CODECOV) or estimated from the file layout (MANUAL). A measurement always outranks an estimate
/repositories/:id/coverage/by-typeCoverage broken down by test type, with the ten least-covered files of each latest run
/repositories/:id/coverage/uploadUpload a coverage report: { format: lcov | cobertura | jacoco, data: '<file contents>', testType?, branch?, commitSha?, pathPrefix?, flags? }. Stored with per-file and per-line detail
/cli/coverageSame upload, addressed by repository name: { repoName, fullName, ...report } — what qg analyze and qg coverage upload call
/repositories/:id/unique-coverageUnique coverage contribution per test type
/repositories/:id/coverage/contributionUnique vs shared lines per test type (computed from file-level data)
/repositories/:id/regressionsRegression alerts for the repository
/repositories/:idSoft-delete a repository
Everything behind the dashboard's health scores, trends, flaky-test and CI-waste panels, and the quality gates.
/organizations/:id/dashboardKPIs: health, coverage by type, flaky tests, pass rate, CI waste, high-risk PRs
/organizations/:id/healthOrganization health score
/organizations/:id/regressionsRepositories whose health dropped, by severity
/organizations/:id/recommendationsPrioritised recommendations
/organizations/:id/trends?period=30d&metrics=score,coverageOrg-level trend series (7d, 30d, 90d, 6m, 1y)
/repositories/:id/trends?period=30dRepository trend series per component
/repositories/:id/trends/regressions?period=30dOldest-vs-newest comparison with warning/critical thresholds
/organizations/:id/flaky-tests?limit=50Flaky tests across the organization, by impact
/repositories/:id/flaky-tests?limit=50Flaky tests of one repository
/repositories/:id/flaky-tests/calculateRecompute flaky metrics from test runs
/organizations/:id/ci-waste?period=30dCI minutes and cost lost to retries and failures
/organizations/:id/quality-gatesQuality gate thresholds (null when unset)
/organizations/:id/quality-gatesSet thresholds: changedCodeCoverage.minimum, overallCoverage.maximumDecrease, criticalRisk.blockMerge, contractChanges.requireContractCoverage, flakyTestImpact.maxAllowed
/repositories/:id/quality-gates/evaluateEvaluate the gates against the latest data
/repositories/:id/health/calculateRecalculate a repository's health score
/repositories/:id/pull-requests?page=1&limit=20Paginated PRs with their latest risk
/organizations/:id/high-risk-prsOpen PRs with risk 60 or more
/pull-requests/:idPR with files, risk snapshots and findings
/pull-requests/:id/riskRisk score and factors (computed on first read)
/pull-requests/:id/risk/calculateRecompute and store a new risk snapshot
/pr-risk/organizations/:id/calculateRecompute risk for every open PR in the organization
/pull-requests/:id/test-gapsFindings: missing API/UI/contract coverage, migrations, untested changes
/repositories/:id/test-mappingsWhich tests cover which files
/repositories/:id/test-mappings/code/:filePathTests covering one file
/repositories/:id/test-mappings/tests/:testFilePathFiles covered by one test
/repositories/:id/test-mappings/buildRebuild mappings from coverage and naming conventions
Push results from CI. Unparsable payloads answer 400 with the parser's message.
/repositories/:id/test-runs/ingest{ format: junit | playwright | pact | json, data, commitSha?, branch? }
/repositories/:id/test-runs?page=1&limit=20Ingested test runs
/repositories/:id/test-runs/:runIdOne run with its executions
/cli/analyzeWhat the qg CLI uploads: { repoName, fullName, analyses: [{ testType, coveragePercent, ... }] }
curl -X POST -H "Authorization: Bearer $QG_TOKEN" -H "Content-Type: application/json" \
"https://qualitygraph.harnessqa.dev/api/repositories/YOUR_REPO_ID/test-runs/ingest" \
-d "{\"format\":\"junit\",\"branch\":\"main\",\"data\":$(jq -Rs . < junit.xml)}"Aggregated stats and trends for the TMS module.
/tms/reports/summary?orgId=:orgIdOverall TMS summary (case counts, pass rates)
/tms/reports/runs?orgId=:orgIdRun history with pass-rate trends
curl -H "Authorization: Bearer $QG_TOKEN" \
"https://qualitygraph.harnessqa.dev/api/tms/reports/summary?orgId=YOUR_ORG_ID"{
"data": {
"totalCases": 142,
"totalRuns": 28,
"overallPassRate": 87.3,
"avgDurationMinutes": 12.5,
"casesByPriority": {"LOW": 20, "MEDIUM": 68, "HIGH": 42, "CRITICAL": 12},
"casesByStatus": {"ACTIVE": 130, "DRAFT": 8, "DEPRECATED": 4},
"recentRuns": [...],
"passRateTrend": [...]
}
}The complete, generated reference with request and response schemas is served by the API itself. Paste your token into the Authorize button to try calls from the browser.
Open Swagger UIThe API uses standard HTTP status codes. Error responses include a descriptive message.
| Code | Status | Description |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource created successfully |
| 400 | Bad Request | Missing required fields or invalid data |
| 401 | Unauthorized | Missing, invalid, or expired token |
| 403 | Forbidden | Token valid but no access to this resource |
| 404 | Not Found | Resource does not exist |
| 500 | Server Error | Internal server error |
{
"statusCode": 401,
"message": "Invalid or expired API token",
"error": "Unauthorized"
}The API does not currently enforce rate limits. Use reasonable request volumes — excessive requests may be throttled in future versions.
Best practices:
POST /tms/cases/bulk) instead of individual createsQualityGraph API v1 — Programmatic QA intelligence. Get API Token · CLI Docs · Sign in · Pricing