Skip to Content

Strategic Tickets

REST endpoints for Strategic Tickets. Base URL https://api.competlab.com; every request needs a CL-API-Key header (see Authentication). Responses wrap in { item } or { items }; errors in { error }.

List the project’s ticket labels

GET/v1/projects/{projectId}/tickets/labels

List the project’s label list — the only labels a ticket may carry, in the order the project picks them. A label is a name and a colour; a ticket names labels by ID.

Returns · { items }

Path parameters

NameTypeDescription
projectIdstringProject ID

Request

curl "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/labels" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY"

Response 200 OK

{ "items": [ { "id": "3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2", "name": "Pricing page", "color": "#3b82f6" } ] }

Errors

StatusCodeMeaning
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "api_key_invalid", "message": "Invalid API key", "status": 401 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

Create a ticket label

POST/v1/projects/{projectId}/tickets/labels

Add a label to the project’s list. Names are unique within the project and compared without regard to case, so Pricing and pricing cannot both exist. Answers with the whole list. Requires a read_write key.

Returns · { items }

Path parameters

NameTypeDescription
projectIdstringProject ID

Request bodyapplication/json

FieldTypeDescription
namestringThe label’s name. Unique within the project, compared without regard to case, at most 40 characters. A project holds at most 50 labels.
colorstringThe label’s colour, as hex — #rrggbb. It is stored lower-case, which is the form every tracker a ticket may be copied into speaks.

Request

curl -X POST "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/labels" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Pricing page","color":"#3b82f6"}'

Response 200 OK

{ "items": [ { "id": "3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2", "name": "Pricing page", "color": "#3b82f6" } ] }

Errors

StatusCodeMeaning
400invalid_parameters · invalid_run_id · invalid_check_id · bad_requestBad request — the payload failed validation.
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
403insufficient_scope · forbiddenThe key is not permitted to access this resource.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "invalid_parameters", "message": "Domain is required", "status": 400 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

Rename or recolour a ticket label

PATCH/v1/projects/{projectId}/tickets/labels/{labelId}

Rename a label, recolour it, or both. It is one write on the list and touches no ticket, because a ticket holds the label’s ID. Answers with the whole list. Requires a read_write key.

Returns · { items }

Path parameters

NameTypeDescription
labelIdstringLabel ID, as the label list returns it
projectIdstringProject ID

Request bodyapplication/json

FieldTypeDescription
namestringThe label’s name. Unique within the project, compared without regard to case, at most 40 characters. A project holds at most 50 labels.
colorstringThe label’s colour, as hex — #rrggbb. It is stored lower-case, which is the form every tracker a ticket may be copied into speaks.

Request

curl -X PATCH "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/labels/3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Pricing page","color":"#3b82f6"}'

Response 200 OK

{ "items": [ { "id": "3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2", "name": "Pricing page", "color": "#3b82f6" } ] }

Errors

StatusCodeMeaning
400invalid_parameters · invalid_run_id · invalid_check_id · bad_requestBad request — the payload failed validation.
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
403insufficient_scope · forbiddenThe key is not permitted to access this resource.
404project_not_foundNo label matches the id in the path.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "invalid_parameters", "message": "Domain is required", "status": 400 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

Delete a ticket label

DELETE/v1/projects/{projectId}/tickets/labels/{labelId}

Remove a label from the project’s list and from every ticket carrying it. Answers with the whole list. Requires a read_write key.

Returns · { items }

Path parameters

NameTypeDescription
labelIdstringLabel ID, as the label list returns it
projectIdstringProject ID

Request

curl -X DELETE "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/labels/3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{}'

Response 200 OK

{ "items": [ { "id": "3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2", "name": "Pricing page", "color": "#3b82f6" } ] }

Errors

StatusCodeMeaning
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
403insufficient_scope · forbiddenThe key is not permitted to access this resource.
404project_not_foundNo label matches the id in the path.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "api_key_invalid", "message": "Invalid API key", "status": 401 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

List the people a ticket can be assigned to

GET/v1/projects/{projectId}/tickets/assignees

List the people a ticket in this project can be assigned to — everyone who is currently a member of the organization, each as the same userId and fullName a ticket already carries for its author and its assignee. This is where assigneeUserId comes from: a create or an update that names anyone else is refused. Somebody invited but not yet joined is not here, because a ticket cannot be assigned to them. Nothing about a person beyond their name and id is returned, on this endpoint or any other.

Returns · { items }

Path parameters

NameTypeDescription
projectIdstringProject ID

Request

curl "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/assignees" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY"

Response 200 OK

{ "items": [ { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" } ] }

Errors

StatusCodeMeaning
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "api_key_invalid", "message": "Invalid API key", "status": 401 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

List project tickets

GET/v1/projects/{projectId}/tickets

List the project’s Strategic Tickets in one flat list — the work the team has taken on, whether a person opened it or your own automation did. Order is the board’s: the columns in board order, and inside each column the order the team put them in. A ticket carries no position of its own on this API, so keep the order you receive. Pass status to read one column. done and dismissed grow without bound, so by default they return only the tickets that reached them in the last 30 days and hasMore is true when there are more; closed=all returns them whole. origin narrows the list to what one source opened, and dimension and briefingRunId to what a Strategic Briefing opened — origin=briefing&briefingRunId={runId} is one edition’s tickets, with {runId} taken from GET /v1/projects/{projectId}/strategic-briefing/history. One edition’s tickets are returned whole, whatever closed says, so the list matches the count the briefing states for it. total is the whole count of the columns this call covers, narrowed the same way — quote that, not the length of items.

Returns · { items, total, hasMore }

Path parameters

NameTypeDescription
projectIdstringProject ID

Query parameters

NameTypeRequiredDescription
statusarray<string>Return only the tickets in these columns — one, or several separated by commas (status=todo,in_progress). Omit it for every column, in board order.
closedstringHow much of done and dismissed to return. recent (the default) returns the tickets that reached those columns within the last 30 days; all returns them whole. The other columns are always whole, whichever you pass. Values: recent, all.
originstringReturn only the tickets one source opened: user — a person working in the app; api — an API key, which is what everything you write through this API carries; briefing or ai_sources — CompetLab itself. No part of CompetLab opens ai_sources tickets today, so that value matches none. Values: user, api, briefing, ai_sources.
dimensionstringReturn only the tickets a Strategic Briefing opened for one part of its analysis. Nothing a person or an API key opens carries a dimension, so this narrows the list to briefing tickets whether or not you also pass origin. agent-readiness is the key for Agent Adoption — how well a site is set up for AI agents to discover, access and read it. The key predates the name and does not change. One of 14 values (e.g. ai-visibility, ai-sources, positioning, …).
briefingRunIdstringReturn only the tickets one Strategic Briefing edition opened. Take the id from GET /v1/projects/{projectId}/strategic-briefing/history, or from meta.runId on a briefing read. Nothing a person or an API key opens carries an edition, so this narrows the list to that edition’s tickets whether or not you also pass origin. An edition’s tickets are returned whole, the finished and dismissed ones included however long ago they were closed — closed has no effect on this read — so the list matches the count the briefing states for that edition.
assigneestringReturn only the tickets one person owns — their user ID, from GET /v1/projects/{projectId}/tickets/assignees. Pass none for the tickets nobody owns. A ticket whose owner has left the organization reads as unassigned everywhere, but is still found here by their ID.
labelIdstringReturn only the tickets carrying one label — its ID, from GET /v1/projects/{projectId}/tickets/labels.
numbernumberReturn the one ticket carrying this number on the board — what a person means by #14. It is a name and not an address: this narrows the list, and every endpoint that acts on a ticket still takes its id. Min 1.
qstringReturn only the tickets whose TITLE contains this text, compared without regard to case. It is matched literally — punctuation is text, not a pattern — and descriptions and comments are not searched.
includearray<string>What each ticket in the list carries. By default a list is an INDEX: every field except the ticket’s own Markdown description, which is returned as an empty string. Pass include=description for the bodies — one ticket’s description runs to thousands of characters, so a whole board fetched with them is large enough to be worth asking for on purpose. GET …/tickets/{ticketId} always returns the description whatever you pass here.

Request

curl "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets?status=todo&closed=recent&origin=briefing&dimension=pricing&briefingRunId=6a3950c06bda450a1372048e&assignee=507f1f77bcf86cd799439031&labelId=3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2&number=14&q=pricing%20page&include=description" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY"

Response 200 OK

{ "items": [ { "id": "507f1f77bcf86cd799439011", "number": 14, "origin": "api", "author": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "briefing": { "runId": "6a3950c06bda450a1372048e", "dimension": "pricing", "estimatedMinutes": 90 }, "title": "Get onto the 2026 buyer guide", "description": "The guide names three of our competitors and not us.", "labels": [ { "id": "3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2", "name": "Pricing page", "color": "#3b82f6" } ], "status": "todo", "statusChangedAt": "2026-09-15T10:00:00.000Z", "assignee": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "dueDate": "2026-10-01", "effort": "medium", "impact": 3, "commentCount": 2, "deletable": true, "createdAt": "2026-09-15T10:00:00.000Z", "updatedAt": "2026-09-16T08:30:00.000Z", "lastActivityAt": "2026-09-17T11:02:00.000Z" } ], "total": 42, "hasMore": false }

Errors

StatusCodeMeaning
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "api_key_invalid", "message": "Invalid API key", "status": 401 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

Create a ticket

POST/v1/projects/{projectId}/tickets

Open a ticket on the project’s board. title and status are required — status names the column it lands in and has no default, because where a ticket belongs depends on who opened it. A new ticket lands at the top of its column. Labels are named by ID from the project’s label list, so create the label there first. A board holds at most 5,000 tickets; past that a create is refused with 400 until some are deleted. The ticket is recorded as opened by an API key — origin is api and author is null — which is how the board tells your automation’s tickets from a person’s. Requires a read_write key.

Returns · { item }

Path parameters

NameTypeDescription
projectIdstringProject ID

Request bodyapplication/json

FieldTypeDescription
titlestringThe ticket’s title.
descriptionstringThe ticket’s description, in Markdown. At most 20000 characters.
statusstringThe column the ticket opens in. Required and with no default: where a ticket belongs depends on who opened it, so a request that does not say is refused rather than landed somewhere plausible. Values: triage, todo, in_progress, done, dismissed.
labelIdsarray<string>IDs from the project’s own label list — read it with the labels endpoint, and create a label there before naming it. At most 10 per ticket, each named once.
assigneeUserIdstringThe user ID of a current member of your organization — list them with GET /v1/projects/{projectId}/tickets/assignees. A user ID from anywhere else is refused.
dueDatestringThe day the ticket is due, as YYYY-MM-DD — a calendar day, with no clock and no time zone.
effortstringHow much work the ticket is. Values: low, medium, high.
impactnumberHow much the ticket matters, from 1 to 4: 1 Minor · 2 Moderate · 3 Significant · 4 Critical.

Request

curl -X POST "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"title":"Get onto the 2026 buyer guide","description":"The guide names three of our competitors and not us.","status":"triage","labelIds":["3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2"],"assigneeUserId":"507f1f77bcf86cd799439031","dueDate":"2026-10-01","effort":"medium","impact":3}'

Response 200 OK

{ "item": { "id": "507f1f77bcf86cd799439011", "number": 14, "origin": "api", "author": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "briefing": { "runId": "6a3950c06bda450a1372048e", "dimension": "pricing", "estimatedMinutes": 90 }, "title": "Get onto the 2026 buyer guide", "description": "The guide names three of our competitors and not us.", "labels": [ { "id": "3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2", "name": "Pricing page", "color": "#3b82f6" } ], "status": "todo", "statusChangedAt": "2026-09-15T10:00:00.000Z", "assignee": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "dueDate": "2026-10-01", "effort": "medium", "impact": 3, "commentCount": 2, "deletable": true, "createdAt": "2026-09-15T10:00:00.000Z", "updatedAt": "2026-09-16T08:30:00.000Z", "lastActivityAt": "2026-09-17T11:02:00.000Z" } }

Errors

StatusCodeMeaning
400invalid_parameters · invalid_run_id · invalid_check_id · bad_requestBad request — the payload failed validation.
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
403insufficient_scope · forbiddenThe key is not permitted to access this resource.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "invalid_parameters", "message": "Domain is required", "status": 400 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

Get a ticket

GET/v1/projects/{projectId}/tickets/{ticketId}

Get one ticket: its description, its labels resolved to name and colour, its assignee, its dates, and how many entries its thread holds. On a ticket a Strategic Briefing opened, briefing names the edition it came from; it is null on every other ticket. An ID that belongs to another project answers 404 exactly as an ID that exists nowhere does.

Returns · { item }

Path parameters

NameTypeDescription
ticketIdstringTicket ID
projectIdstringProject ID

Request

curl "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/507f1f77bcf86cd799439011" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY"

Response 200 OK

{ "item": { "id": "507f1f77bcf86cd799439011", "number": 14, "origin": "api", "author": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "briefing": { "runId": "6a3950c06bda450a1372048e", "dimension": "pricing", "estimatedMinutes": 90 }, "title": "Get onto the 2026 buyer guide", "description": "The guide names three of our competitors and not us.", "labels": [ { "id": "3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2", "name": "Pricing page", "color": "#3b82f6" } ], "status": "todo", "statusChangedAt": "2026-09-15T10:00:00.000Z", "assignee": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "dueDate": "2026-10-01", "effort": "medium", "impact": 3, "commentCount": 2, "deletable": true, "createdAt": "2026-09-15T10:00:00.000Z", "updatedAt": "2026-09-16T08:30:00.000Z", "lastActivityAt": "2026-09-17T11:02:00.000Z" } }

Errors

StatusCodeMeaning
400invalid_parameters · invalid_run_id · invalid_check_id · bad_requestBad request — the payload failed validation.
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
404project_not_foundNo ticket matches the id in the path.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "invalid_parameters", "message": "Domain is required", "status": 400 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

Update a ticket

PATCH/v1/projects/{projectId}/tickets/{ticketId}

Change a ticket’s content and planning fields. Omit a field to leave it as it is, send a value to replace it, send null to clear it. The description and the labels are cleared by their own empty value instead — "" and [] — because for those an empty value is a real one. A column, and a place inside one, are a move and are not accepted here, so a ticket can never change column as a side effect of an edit. Requires a read_write key.

Returns · { item }

Path parameters

NameTypeDescription
ticketIdstringTicket ID
projectIdstringProject ID

Request bodyapplication/json

FieldTypeDescription
titlestringThe ticket’s title.
descriptionstringThe ticket’s description, in Markdown. At most 20000 characters. Send "" to clear it.
labelIdsarray<string>IDs from the project’s own label list — read it with the labels endpoint, and create a label there before naming it. At most 10 per ticket, each named once. The list replaces what the ticket holds; send [] to clear it.
assigneeUserIdstring | nullThe user ID of a current member of your organization — list them with GET /v1/projects/{projectId}/tickets/assignees. A user ID from anywhere else is refused. Send null to leave the ticket unassigned. Nullable.
dueDatestring | nullThe day the ticket is due, as YYYY-MM-DD — a calendar day, with no clock and no time zone. Send null to take the due date off. Nullable.
effortstring | nullHow much work the ticket is. Send null to take it off. Values: low, medium, high. Nullable.
impactnumber | nullHow much the ticket matters, from 1 to 4: 1 Minor · 2 Moderate · 3 Significant · 4 Critical. Send null to take it off. Nullable.

Request

curl -X PATCH "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/507f1f77bcf86cd799439011" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"title":"Get onto the 2026 buyer guide","description":"The guide names three of our competitors and not us.","labelIds":["3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2"],"assigneeUserId":"507f1f77bcf86cd799439031","dueDate":"2026-10-01","effort":"medium","impact":3}'

Response 200 OK

{ "item": { "id": "507f1f77bcf86cd799439011", "number": 14, "origin": "api", "author": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "briefing": { "runId": "6a3950c06bda450a1372048e", "dimension": "pricing", "estimatedMinutes": 90 }, "title": "Get onto the 2026 buyer guide", "description": "The guide names three of our competitors and not us.", "labels": [ { "id": "3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2", "name": "Pricing page", "color": "#3b82f6" } ], "status": "todo", "statusChangedAt": "2026-09-15T10:00:00.000Z", "assignee": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "dueDate": "2026-10-01", "effort": "medium", "impact": 3, "commentCount": 2, "deletable": true, "createdAt": "2026-09-15T10:00:00.000Z", "updatedAt": "2026-09-16T08:30:00.000Z", "lastActivityAt": "2026-09-17T11:02:00.000Z" } }

Errors

StatusCodeMeaning
400invalid_parameters · invalid_run_id · invalid_check_id · bad_requestBad request — the payload failed validation.
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
403insufficient_scope · forbiddenThe key is not permitted to access this resource.
404project_not_foundNo ticket matches the id in the path.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "invalid_parameters", "message": "Domain is required", "status": 400 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

Delete a ticket

DELETE/v1/projects/{projectId}/tickets/{ticketId}

Delete a ticket and its thread. A ticket a Strategic Briefing opened cannot be deleted — move it to dismissed instead, so that what opened it does not open it again. Read deletable on the ticket first; deleting one that is not is refused with 400. Requires a read_write key.

Returns · { item }

Path parameters

NameTypeDescription
ticketIdstringTicket ID
projectIdstringProject ID

Request

curl -X DELETE "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/507f1f77bcf86cd799439011" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{}'

Response 200 OK

{ "item": { "success": true, "id": "507f1f77bcf86cd799439011" } }

Errors

StatusCodeMeaning
400invalid_parameters · invalid_run_id · invalid_check_id · bad_requestBad request — the payload failed validation.
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
403insufficient_scope · forbiddenThe key is not permitted to access this resource.
404project_not_foundNo ticket matches the id in the path.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "invalid_parameters", "message": "Domain is required", "status": 400 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

Move a ticket

PATCH/v1/projects/{projectId}/tickets/{ticketId}/move

Move a ticket to another column, or reorder it inside the one it is in. Name the destination column and the two tickets it will sit between: beforeId is the one directly above it, afterId the one directly below. Name either or both. To put a ticket at the top of a column, send the ticket currently first there as afterId and no beforeId; omitting both puts it at the bottom — which is the column’s first card when the column is empty. There is no position and no index to compute — you name neighbours you can see and the server works out the order, so two clients moving tickets at the same time converge on one order instead of overwriting each other. A neighbour that has since moved or been deleted is ignored and the ticket lands at the nearest place that is still true: a move never fails because your view of the board was a moment out of date. Reordering inside a column leaves statusChangedAt alone. Requires a read_write key.

Returns · { item }

Path parameters

NameTypeDescription
ticketIdstringTicket ID
projectIdstringProject ID

Request bodyapplication/json

FieldTypeDescription
statusstringThe column the ticket ends up in. Always the destination, never a change flag — reordering a ticket inside the column it already sits in names that column. Values: triage, todo, in_progress, done, dismissed.
beforeIdstringThe ticket that will sit directly ABOVE this one. Name either neighbour or both. Omitting both puts the ticket at the BOTTOM of the destination column — which is its first card when that column is empty.
afterIdstringThe ticket that will sit directly BELOW this one. To put a ticket at the TOP of a column, send the ticket currently first there as afterId, and no beforeId.

Request

curl -X PATCH "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/507f1f77bcf86cd799439011/move" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"status":"in_progress","beforeId":"507f1f77bcf86cd799439012","afterId":"507f1f77bcf86cd799439013"}'

Response 200 OK

{ "item": { "id": "507f1f77bcf86cd799439011", "number": 14, "origin": "api", "author": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "briefing": { "runId": "6a3950c06bda450a1372048e", "dimension": "pricing", "estimatedMinutes": 90 }, "title": "Get onto the 2026 buyer guide", "description": "The guide names three of our competitors and not us.", "labels": [ { "id": "3f2a9c6e-1b77-4c1e-9a54-5c7b2f0a91d2", "name": "Pricing page", "color": "#3b82f6" } ], "status": "todo", "statusChangedAt": "2026-09-15T10:00:00.000Z", "assignee": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "dueDate": "2026-10-01", "effort": "medium", "impact": 3, "commentCount": 2, "deletable": true, "createdAt": "2026-09-15T10:00:00.000Z", "updatedAt": "2026-09-16T08:30:00.000Z", "lastActivityAt": "2026-09-17T11:02:00.000Z" } }

Errors

StatusCodeMeaning
400invalid_parameters · invalid_run_id · invalid_check_id · bad_requestBad request — the payload failed validation.
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
403insufficient_scope · forbiddenThe key is not permitted to access this resource.
404project_not_foundNo ticket matches the id in the path.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "invalid_parameters", "message": "Domain is required", "status": 400 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

List a ticket’s comments

GET/v1/projects/{projectId}/tickets/{ticketId}/comments

List a ticket’s thread, oldest first and whole — nothing pages it, so nothing is counted twice. Each entry says what wrote it: a person working in the app, or an API key.

Returns · { items }

Path parameters

NameTypeDescription
ticketIdstringTicket ID
projectIdstringProject ID

Request

curl "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/507f1f77bcf86cd799439011/comments" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY"

Response 200 OK

{ "items": [ { "id": "507f1f77bcf86cd799439021", "ticketId": "507f1f77bcf86cd799439011", "origin": "api", "author": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "body": "Reached the editor on 14 Sep; they update the guide quarterly.", "edited": false, "createdAt": "2026-09-15T10:05:00.000Z", "updatedAt": "2026-09-15T10:05:00.000Z" } ] }

Errors

StatusCodeMeaning
400invalid_parameters · invalid_run_id · invalid_check_id · bad_requestBad request — the payload failed validation.
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
404project_not_foundNo ticket matches the id in the path.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "invalid_parameters", "message": "Domain is required", "status": 400 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

Add a ticket comment

POST/v1/projects/{projectId}/tickets/{ticketId}/comments

Add an entry to a ticket’s thread, written in Markdown. The entry is recorded as written by an API key. A thread holds at most 500 entries. Requires a read_write key.

Returns · { item }

Path parameters

NameTypeDescription
ticketIdstringTicket ID
projectIdstringProject ID

Request bodyapplication/json

FieldTypeDescription
bodystringThe entry’s text, in Markdown. At most 20000 characters. It is stored exactly as it is sent — leading whitespace is a code block in the dialect that draws it, so nothing is trimmed off the ends.

Request

curl -X POST "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/507f1f77bcf86cd799439011/comments" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"body":"Reached the editor on 14 Sep; they update the guide quarterly."}'

Response 200 OK

{ "item": { "id": "507f1f77bcf86cd799439021", "ticketId": "507f1f77bcf86cd799439011", "origin": "api", "author": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "body": "Reached the editor on 14 Sep; they update the guide quarterly.", "edited": false, "createdAt": "2026-09-15T10:05:00.000Z", "updatedAt": "2026-09-15T10:05:00.000Z" } }

Errors

StatusCodeMeaning
400invalid_parameters · invalid_run_id · invalid_check_id · bad_requestBad request — the payload failed validation.
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
403insufficient_scope · forbiddenThe key is not permitted to access this resource.
404project_not_foundNo ticket matches the id in the path.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "invalid_parameters", "message": "Domain is required", "status": 400 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

Edit a ticket comment

PATCH/v1/projects/{projectId}/tickets/{ticketId}/comments/{commentId}

Rewrite an entry this API wrote. An entry a person wrote in the app is theirs to edit there, and rewriting it here answers 403 forbidden. The text is the only part of an entry anyone may change. Requires a read_write key.

Returns · { item }

Path parameters

NameTypeDescription
ticketIdstringTicket ID
commentIdstringComment ID
projectIdstringProject ID

Request bodyapplication/json

FieldTypeDescription
bodystringThe entry’s text, in Markdown. At most 20000 characters. It is stored exactly as it is sent — leading whitespace is a code block in the dialect that draws it, so nothing is trimmed off the ends.

Request

curl -X PATCH "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/507f1f77bcf86cd799439011/comments/507f1f77bcf86cd799439021" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"body":"Reached the editor on 14 Sep; they update the guide quarterly."}'

Response 200 OK

{ "item": { "id": "507f1f77bcf86cd799439021", "ticketId": "507f1f77bcf86cd799439011", "origin": "api", "author": { "userId": "507f1f77bcf86cd799439011", "fullName": "Dana Ellis" }, "body": "Reached the editor on 14 Sep; they update the guide quarterly.", "edited": false, "createdAt": "2026-09-15T10:05:00.000Z", "updatedAt": "2026-09-15T10:05:00.000Z" } }

Errors

StatusCodeMeaning
400invalid_parameters · invalid_run_id · invalid_check_id · bad_requestBad request — the payload failed validation.
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
403insufficient_scope · forbiddenThe key is not permitted to access this resource.
404project_not_foundNo comment matches the id in the path.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "invalid_parameters", "message": "Domain is required", "status": 400 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

Delete a ticket comment

DELETE/v1/projects/{projectId}/tickets/{ticketId}/comments/{commentId}

Delete an entry from a ticket’s thread. A key reaches any entry on the ticket, whoever wrote it: a key is issued by an owner or an admin and acts with that authority. Requires a read_write key.

Returns · { item }

Path parameters

NameTypeDescription
ticketIdstringTicket ID
commentIdstringComment ID
projectIdstringProject ID

Request

curl -X DELETE "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/tickets/507f1f77bcf86cd799439011/comments/507f1f77bcf86cd799439021" \ -H "CL-API-Key: YOUR_COMPETLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{}'

Response 200 OK

{ "item": { "success": true, "id": "507f1f77bcf86cd799439011" } }

Errors

StatusCodeMeaning
400invalid_parameters · invalid_run_id · invalid_check_id · bad_requestBad request — the payload failed validation.
401api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scopeThe CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope.
402subscription_requiredRequest failed.
403insufficient_scope · forbiddenThe key is not permitted to access this resource.
404project_not_foundNo comment matches the id in the path.
429rate_limit_exceededRate limit exceeded — retry with backoff.

Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.

{ "error": { "code": "invalid_parameters", "message": "Domain is required", "status": 400 } }

Transient failures (429, 5xx) are safe to retry with exponential backoff.

Last updated on