Support Tickets
Read your organization's support tickets — bug reports and feature requests — programmatically.
Every request is scoped to the organization that owns your API key. There is no organization parameter, and no way to reach another organization's tickets.
These endpoints pass the responses from our ticketing provider (Atlassian Jira
Service Management) through unchanged, which is why they live under
/api/v1/atlassian. If we move to a different provider, the new endpoints will
live under a new path and these will be deprecated on a published timeline
rather than changed underneath you.
Listing tickets
GET /api/v1/atlassian/issues
Returns your organization's tickets, most recently changed first.
| Parameter | Description |
|---|---|
updated_since | Only tickets changed at or after this time. RFC 3339 with an offset, e.g. 2026-08-12T00:00:00Z |
created_since | Only tickets raised at or after this time. RFC 3339 with an offset |
status | open, closed, or all. Defaults to all |
limit | Tickets per page, 1–100. Defaults to 50 |
next_page_token | Cursor for the next page, taken from the previous response |
curl -H "Authorization: Bearer $TVLABS_API_KEY" \
"https://tvlabs.ai/api/v1/atlassian/issues?status=open&limit=25"
The response is the provider's search body:
{
"issues": [
{
"key": "TVL-675",
"fields": {
"summary": "Unable to log in with valid credentials",
"status": { "name": "Pending Customer Response" },
"created": "2026-08-11T07:29:49.471-0400",
"updated": "2026-08-12T04:57:37.772-0400",
"reporter": { "displayName": "Kiran Reddy" },
"customfield_10051": { "name": "Time to first response", "completedCycles": [] }
}
}
],
"names": { "customfield_10051": "Time to first response" },
"nextPageToken": "CAEaAggD"
}
Ticket fields keep the provider's names, including opaque ids like
customfield_10051. The names object maps those ids to their display names,
so you can resolve them at runtime instead of hard-coding them. It sits at the
top level of the response on this endpoint and on /issues/bulk, alongside
issues; on the single-ticket endpoint it sits alongside that ticket's own
fields.
To page through results, pass the nextPageToken from one response as
next_page_token on the next request. The last page has no nextPageToken.
A cursor that has expired or been altered returns 400 rather than 502, so
don't retry it — start the walk again from the first page.
Descriptions and comments are not included
Listing responses carry the ticket's metadata and SLA state but not its description or conversation, so walking a large backlog stays fast. Fetch those per ticket once you know which ones you care about.
Syncing changes
updated_since is the intended way to keep a local copy current: list the
tickets that changed since your last sync, then fetch the detail for just those.
curl -H "Authorization: Bearer $TVLABS_API_KEY" \
"https://tvlabs.ai/api/v1/atlassian/issues?updated_since=2026-08-12T00:00:00Z"
Two things to know:
- Comparisons are minute-accurate. A window may return a ticket you already
hold. De-duplicate on
keytogether withfields.updated. - Timestamps carry the provider's offset, as in the example above. Parse them as RFC 3339 rather than assuming UTC.
Advance your cursor using the largest fields.updated you actually received,
not your own clock, so a ticket changed mid-sync isn't skipped.
Fetching many tickets at once
POST /api/v1/atlassian/issues/bulk
Fetches up to 100 tickets by key in a single request, each carrying everything
the single-ticket endpoint returns, descriptions included. This is the companion
to updated_since: take the keys that changed and pull them in one round trip
instead of one request per ticket.
curl -X POST \
-H "Authorization: Bearer $TVLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"keys": ["TVL-675", "TVL-676"]}' \
https://tvlabs.ai/api/v1/atlassian/issues/bulk
{
"issues": [
{
"key": "TVL-675",
"fields": { "summary": "Unable to log in with valid credentials" }
},
{
"key": "TVL-676",
"fields": { "summary": "Playback stalls on 4K streams" }
}
],
"names": { "customfield_10051": "Time to first response" }
}
One names map covers every ticket in the response, at the top level alongside
issues, the same as on the listing endpoint.
Tickets that don't exist, and tickets belonging to another organization, are
absent from issues; the two cases are indistinguishable. Diff the keys you
asked for against the keys you got back to find the gaps.
Comments aren't included — fetch those per ticket.
Fetching one ticket
GET /api/v1/atlassian/issues/:key
Returns a single ticket including its description, plus everything the listing
carries.
curl -H "Authorization: Bearer $TVLABS_API_KEY" \
https://tvlabs.ai/api/v1/atlassian/issues/TVL-675
Tickets that belong to another organization return 404, the same as a ticket
that doesn't exist.
Fetching a ticket's comments
GET /api/v1/atlassian/issues/:key/comments
Returns the public conversation on a ticket. Internal notes between our support engineers are never included.
| Parameter | Description |
|---|---|
start | Index of the first comment. Defaults to 0 |
limit | Comments per page, 1–100. Defaults to 50 |
curl -H "Authorization: Bearer $TVLABS_API_KEY" \
https://tvlabs.ai/api/v1/atlassian/issues/TVL-675/comments
Use isLastPage in the response to decide whether to request another page.
Rate limits
These endpoints draw on a quota we share with our ticketing provider, so each organization gets a budget of upstream calls per minute. Listing a page and fetching one ticket each cost one call; fetching a batch, or a ticket's comments, costs two.
Batching is the cheapest way to stay inside the budget: one hundred tickets cost
two calls through /issues/bulk and a hundred calls one at a time.
Every response carries your current standing:
| Header | Meaning |
|---|---|
ratelimit-limit | Calls available per window |
ratelimit-remaining | Calls left in the current window |
ratelimit-reset | Seconds until the window resets |
Exceeding the budget returns 429 with a retry-after header. If our provider
throttles us, you'll get 503, also with retry-after. Back off and retry in
both cases; a sync that pauses for the stated interval will finish.
Errors
| Status | error | Meaning |
|---|---|---|
| 400 | BadRequest | A parameter was malformed — check the message |
| 401 | — | Missing or invalid API key |
| 404 | NotFound | No such ticket, or it belongs to another organization |
| 404 | SupportNotConfigured | Your organization isn't linked to a support project yet — contact [email protected] |
| 429 | TooManyRequests | Your organization's budget is spent |
| 502 | UpstreamError | Our ticketing provider could not be reached |
| 503 | UpstreamRateLimited | Our ticketing provider is throttling us |