TCM Automation Docs

Integrations API reference

Server-to-server REST endpoints under /api/v1/integrations/**. See Integrations for the conceptual model.

Authentication

Every request carries a bearer integration token in the standard Authorization header:

Authorization: Bearer tcm_int_<your-token>

Tokens are minted in the TCM web UI under Admin → Integrations → (pick one) → Mint token. The plaintext is shown once at mint time and never again.

List defects

GET /api/v1/integrations/defects — scope: defects:read

Query parameters

All parameters are optional.

ParamTypeNotes
external_project_idstringYour project ID (pre-mapped). Unknown returns an empty page.
external_application_idstringYour application ID (pre-mapped).
external_module_idstringYour module ID (pre-mapped).
external_assignee_user_idstringYour user ID — filters to defects assigned to that user.
project_iduuidOur internal UUID, accepted as an alternative to external_project_id.
application_iduuidOur internal application UUID, alternative to external_application_id.
module_iduuidOur internal module UUID, alternative to external_module_id.
status_iduuidDefect-status UUID. List statuses per project from your web UI.
status_namestringExact match on our workflow name (e.g. Open, In Progress, Closed). Cross-project: if two projects both have an Open status, this matches defects in both. Pair with project_id if you want to scope by project too.
severityenumcritical | high | medium | low
priorityenump1 | p2 | p3 | p4
qstringILIKE on defect title or external_key (e.g. BUG_0007).
updated_sinceRFC3339Returns defects with updated_at >= this. Use for incremental sync. See the gap note in concepts.
limitintDefault 50, max 200.
offsetintDefault 0.

Response

{
  "items": [
    {
      "id": "uuid",
      "external_key": "BUG_0007",
      "title": "Login broken on Safari 16",
      "severity": "high",
      "priority": "p2",
      "bug_type": "functional",
      "status":      { "id": "uuid", "name": "In Review", "color": "#..." },
      "project":     { "id": "uuid", "external_id": "PROJ-42" },
      "application": { "id": "uuid", "external_id": "APP-9"  },
      "module":      { "id": "uuid", "external_id": "MOD-3"  },
      "reporter": {
        "id": "uuid", "external_id": "emp-7",
        "full_name": "Asha Patel", "email": "asha@example.com"
      },
      "assignees": [
        { "id": "uuid", "external_id": "emp-22", "full_name": "Ravi K.", "email": "ravi@..." }
      ],
      "created_at": "2026-05-12T11:23:04Z",
      "updated_at": "2026-05-18T09:01:55Z"
    }
  ],
  "total_count": 142,
  "limit": 50,
  "offset": 0
}
  • application and module are omitted when the defect has none.
  • Any external_id field is omitted when the integrator hasn’t pre-mapped that internal entity. The internal id is always present.

Examples

first-page.sh
curl -H "Authorization: Bearer $IT" \
  "https://YOUR_HOST/api/v1/integrations/defects?limit=50"
filter-by-project.sh
curl -H "Authorization: Bearer $IT" \
  "https://YOUR_HOST/api/v1/integrations/defects?external_project_id=PROJ-42"
incremental-sync.sh
# Run every 5 minutes; persist the timestamp client-side.
SINCE="2026-05-18T00:00:00Z"
curl -H "Authorization: Bearer $IT" \
  "https://YOUR_HOST/api/v1/integrations/defects?updated_since=$SINCE&limit=200"
severity-and-assignee.sh
curl -H "Authorization: Bearer $IT" \
  "https://YOUR_HOST/api/v1/integrations/defects?severity=critical&external_assignee_user_id=emp-22"
by-status-name.sh
# All defects currently in "In Progress" across the integrator's view
curl -H "Authorization: Bearer $IT" \
  "https://YOUR_HOST/api/v1/integrations/defects?status_name=In%20Progress"
by-app-and-module.sh
# Filter by application + module using either internal UUIDs or external IDs.
# Pick one of the two on each axis.
curl -H "Authorization: Bearer $IT" \
  "https://YOUR_HOST/api/v1/integrations/defects?application_id=<uuid>&module_id=<uuid>"

Create a build

POST /api/v1/integrations/builds — scope: builds:write

Records a build for a project + application. Identify them by internal UUID (project_id / application_id) or by your external IDs (external_project_id / external_application_id), which resolve through this integration’s mappings.

Body

FieldTypeNotes
project_id / external_project_iduuid / stringRequired — provide one. Internal UUID, or your mapped external ID.
application_id / external_application_iduuid / stringRequired — provide one.
build_nostringRequired. The version, e.g. 1.4.0-rc2.
environmentenumdev | sit | uat | pre_prod | production
descriptionstringOptional.
tagsstring[]Optional.

Example

create-build.sh
curl -X POST -H "Authorization: Bearer $IT" \
     -H "Content-Type: application/json" \
     -d '{
           "external_project_id": "PROJ-42",
           "external_application_id": "APP-9",
           "build_no": "1.4.0-rc2",
           "environment": "uat",
           "tags": ["ci", "nightly"]
         }' \
     "https://YOUR_HOST/api/v1/integrations/builds"

Success: 201 with the created build — the internal id, build_no, environment, and the resolved project_id / application_id.

Unmapped IDs return 400
If an external_* value isn’t mapped yet, POST /builds returns 400 integration.unmapped_project / integration.unmapped_application — push the mapping first (see Mappings).

Record a result / attach a file

Address an execution by (run, test case) — both internal UUIDs — instead of an execution id. Scope: executions:write.

Report — result + attachment in one call

POST /api/v1/integrations/runs/{run_id}/testcases/{test_case_id}/report — a multipart form that records the result and links a file in a single request. Fields: result (required), optional duration_ms, worker_id, error_message, error_trace, logs_url, remark, and an optional file part.

report.sh
curl -X POST -H "Authorization: Bearer $IT" \
     -F "result=failed" \
     -F "duration_ms=1543" \
     -F "worker_id=runner-7" \
     -F "file=@screenshot.png" \
     "https://YOUR_HOST/api/v1/integrations/runs/<run-id>/testcases/<test-case-id>/report"

Omit the file part to just record the result.

Record a result (only)

PATCH /api/v1/integrations/runs/{run_id}/testcases/{test_case_id}/result

record-result.sh
curl -X PATCH -H "Authorization: Bearer $IT" \
     -H "Content-Type: application/json" \
     -d '{"result": "passed", "duration_ms": 1543}' \
     "https://YOUR_HOST/api/v1/integrations/runs/<run-id>/testcases/<test-case-id>/result"

result is one of passed | failed | blocked | skipped | partial. Optional: duration_ms, worker_id, error_message, error_trace, logs_url, remark, step_results, screenshots, metrics.

Attach a file

POST /api/v1/integrations/runs/{run_id}/testcases/{test_case_id}/attachments — multipart, single file field.

attach.sh
curl -X POST -H "Authorization: Bearer $IT" \
     -F "file=@screenshot.png" \
     "https://YOUR_HOST/api/v1/integrations/runs/<run-id>/testcases/<test-case-id>/attachments"
Ambiguous test case → 409
If the test case appears more than once in the run (two executions), these return 409 execution.ambiguous — record/attach by the execution id instead. 404 if the test case isn’t in the run.

Mappings

Per-integration ID bridge for projects, applications, modules, and users. Manage from the admin UI or from the integrator’s bootstrap script using a token with mappings:read / mappings:write scopes.

{kind} below is one of: project | application | module | user.

Bulk upsert

POST /api/v1/integrations/mappings/{kind} — scope: mappings:write

Body is an array. Conflict on the PK (tenant, integration, kind, internal_id) updates the external_id. Conflict on the secondary unique (tenant, integration, kind, external_id) returns 400 integration.mapping_taken — that means the integrator is trying to map the same external ID to two different internal entities, which is never allowed.

bootstrap.sh
curl -X POST -H "Authorization: Bearer $IT" \
     -H "Content-Type: application/json" \
     -d '[
           {"internal_id": "11111111-1111-1111-1111-111111111111", "external_id": "PROJ-42"},
           {"internal_id": "22222222-2222-2222-2222-222222222222", "external_id": "PROJ-43"}
         ]' \
     "https://YOUR_HOST/api/v1/integrations/mappings/project"

Success: 204 No Content.

List

GET /api/v1/integrations/mappings/{kind} — scope: mappings:read

curl -H "Authorization: Bearer $IT" \
  "https://YOUR_HOST/api/v1/integrations/mappings/project?limit=200"
{
  "items": [
    { "internal_id": "uuid", "external_id": "PROJ-42" },
    { "internal_id": "uuid", "external_id": "PROJ-43" }
  ],
  "total_count": 2,
  "limit": 50,
  "offset": 0
}

Delete one

DELETE /api/v1/integrations/mappings/{kind}/{external_id} — scope: mappings:write

curl -X DELETE -H "Authorization: Bearer $IT" \
  "https://YOUR_HOST/api/v1/integrations/mappings/project/PROJ-42"

Success: 204. Missing mapping: 404 integration.mapping_not_found.

Error envelope

Every 4xx / 5xx response uses the same JSON shape:

{ "code": "integration.scope_missing", "message": "missing required scope" }
StatusCodeWhen
401auth.invalid_tokenBearer header missing / token unknown
401auth.token_expiredToken past its expires_at
401auth.token_revokedToken revoked by an admin (or its integration was deleted)
403integration.scope_missingToken doesn’t hold the scope this route requires
400integration.invalid_kindPath {kind} isn’t project / application / module / user
400integration.mapping_takenExternal ID already mapped to a different internal entity
400bad_updated_sinceupdated_since isn’t valid RFC3339
404integration.mapping_not_foundDelete on a mapping that doesn’t exist
Unknown external_id is 200, not 404
Filtering by an external_* value that the integrator hasn’t mapped yet returns 200 with { items: [], total_count: 0 }. “No defects match an unmapped project” is a correct semantic answer, not an error.

Pagination strategy

Use limit + offset. The API caps limit at 200. For backlog scans, page until a response returns fewer than limit items. For ongoing sync, use updated_since with the timestamp of the most-recent updated_at you’ve already processed (minus a small skew, e.g. 30s, to ride out clock drift).