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.
| Param | Type | Notes |
|---|---|---|
external_project_id | string | Your project ID (pre-mapped). Unknown returns an empty page. |
external_application_id | string | Your application ID (pre-mapped). |
external_module_id | string | Your module ID (pre-mapped). |
external_assignee_user_id | string | Your user ID — filters to defects assigned to that user. |
project_id | uuid | Our internal UUID, accepted as an alternative to external_project_id. |
application_id | uuid | Our internal application UUID, alternative to external_application_id. |
module_id | uuid | Our internal module UUID, alternative to external_module_id. |
status_id | uuid | Defect-status UUID. List statuses per project from your web UI. |
status_name | string | Exact 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. |
severity | enum | critical | high | medium | low |
priority | enum | p1 | p2 | p3 | p4 |
q | string | ILIKE on defect title or external_key (e.g. BUG_0007). |
updated_since | RFC3339 | Returns defects with updated_at >= this. Use for incremental sync. See the gap note in concepts. |
limit | int | Default 50, max 200. |
offset | int | Default 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
}applicationandmoduleare omitted when the defect has none.- Any
external_idfield is omitted when the integrator hasn’t pre-mapped that internal entity. The internalidis always present.
Examples
curl -H "Authorization: Bearer $IT" \
"https://YOUR_HOST/api/v1/integrations/defects?limit=50"curl -H "Authorization: Bearer $IT" \
"https://YOUR_HOST/api/v1/integrations/defects?external_project_id=PROJ-42"# 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"curl -H "Authorization: Bearer $IT" \
"https://YOUR_HOST/api/v1/integrations/defects?severity=critical&external_assignee_user_id=emp-22"# 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"# 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.
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" }| Status | Code | When |
|---|---|---|
| 401 | auth.invalid_token | Bearer header missing / token unknown |
| 401 | auth.token_expired | Token past its expires_at |
| 401 | auth.token_revoked | Token revoked by an admin (or its integration was deleted) |
| 403 | integration.scope_missing | Token doesn’t hold the scope this route requires |
| 400 | integration.invalid_kind | Path {kind} isn’t project / application / module / user |
| 400 | integration.mapping_taken | External ID already mapped to a different internal entity |
| 400 | bad_updated_since | updated_since isn’t valid RFC3339 |
| 404 | integration.mapping_not_found | Delete on a mapping that doesn’t exist |
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).