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

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