TR-AI REST API
Programmatic access to ticket resolution, analytics, and your knowledge base. RESTful, JSON, and built for developers.
https://api.ticketresolve.ai/v1All requests require an API key in the Authorization header: "Authorization: Bearer <key>". Generate keys from your dashboard under Settings → API Keys. A key belongs to one trial account, carries one or more scopes, and is shown only once when you create it — revoke it there at any time.
Quick start
Create a key with the resolve scope, then submit a ticket:
curl https://api.ticketresolve.ai/v1/tickets/resolve \
-H "Authorization: Bearer $TR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject": "Cannot log in", "body": "I reset my password but still get an error.", "requester_email": "jo@example.com", "channel": "email"}'Scopes
readRead tickets, analytics, known issues and knowledge base articles; search the knowledge base.resolveSubmit tickets for AI resolution.writeSubmit CSAT feedback on tickets; create and update known issues.kbAdd, update and delete knowledge base articles.Errors
Every error response has the same JSON shape. "param" is present when a specific parameter is at fault; quote "request_id" (also sent as the X-Request-Id header on every response) when contacting support.
{
"error": {
"type": "invalid_request_error",
"code": "missing_parameter",
"message": "subject is required.",
"param": "subject",
"request_id": "req_8f3c2a91d04b7e6a5c1f2d33"
}
}| 400 | invalid_request_error | missing_parameter, invalid_parameter, invalid_json, invalid_body |
| 401 | authentication_error | missing_api_key, invalid_api_key (unknown or revoked) |
| 403 | permission_error | insufficient_scope, automation_blocked (AI paused, trial expired or inactive) |
| 404 | not_found_error | ticket_not_found, article_not_found, route_not_found |
| 409 | invalid_request_error | ticket_not_completed (feedback on a ticket still processing) |
| 413 | invalid_request_error | payload_too_large |
| 429 | rate_limit_error | rate_limit_exceeded |
| 500 | api_error | internal_error, resolution_failed — safe to retry |
Rate limits
Limits depend on your plan: each API key may make 60 requests per minute on Starter, 120 on Bronze and 600 on Growth Pro (and during the free trial). POST /tickets/resolve allows a quarter of that — 15, 30 and 150 requests per minute. GET /v1/me returns your key's current limits. Limits are token buckets, so short bursts up to the limit are fine. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix time, in seconds, when the bucket is full again). Over the limit you get 429 rate_limit_exceeded with a Retry-After header in seconds.
Pagination
List endpoints take page (default 1) and per_page (default 25, max 100) and return { "object": "list", "data": [...], "pagination": { "page", "per_page", "total", "total_pages", "has_more" } }. Results are newest first.
Dates and bodies
Dates are ISO 8601 — a date (2026-11-01) or a date-time with a time zone (2026-11-01T09:30:00Z) — and timestamps in responses are UTC. A date-only "to" includes that whole day. Request bodies are JSON (Content-Type: application/json), up to 1 MB.
Tickets
/tickets/resolveSubmit a ticket for AI resolutionscope: resolveRuns the ticket through the resolution engine and returns the drafted reply, a confidence score, the sources used and a recommended action — nothing is posted to your help desk; you decide what to send. Tickets up to 2,000 tokens (about 8,000 characters of subject + body) are answered synchronously with 200. Longer tickets return 202 with status "processing": poll GET /tickets/{id} or subscribe to the ticket.resolved, ticket.triaged and ticket.review_pending webhooks. Your account settings apply: if AI automation is paused or the trial has expired the request fails with 403 automation_blocked, confidence thresholds decide resolved / pending / escalated, and in assisted mode resolved tickets come back with recommended_action "hold_for_review". recommended_action is one of send_reply, hold_for_review, escalate_with_reply (hand to a human and send the reply as an acknowledgement) or escalate (hand to a human, send nothing).
subjectstringbodyrequiredTicket subject line (max 1,000 characters)bodystringbodyrequiredFull ticket body text (max 100,000 characters)requester_emailstringbodyoptionalCustomer email for history lookuprequester_namestringbodyoptionalCustomer name, used to personalise the replychannelstringbodyoptionalSource channel: email, chat, web, api (default: api){
"id": "tkt_5b1e0c9a7d2f4e8a91c3b6d0",
"object": "ticket",
"source": "api",
"status": "completed",
"resolution_type": "resolved",
"recommended_action": "send_reply",
"subject": "Cannot log in",
"channel": "email",
"requester_email": "jo@example.com",
"confidence_score": 0.912,
"reply": {
"subject": "Re: Cannot log in",
"body": "Hi Jo, after a password reset you need to sign out of all devices…"
},
"escalation_reason": null,
"internal_note": null,
"sources": [
{
"id": 48213,
"type": "kb",
"similarity": 0.83,
"url": "https://help.example.com/articles/reset-password"
}
],
"resolution_log_id": 90211,
"csat": null,
"error": null,
"created_at": "2026-11-05T09:30:12.000Z",
"completed_at": "2026-11-05T09:30:16.000Z"
}/tickets/{id}Get ticket resolution detailsscope: readRetrieve the full resolution record for a ticket, including confidence score, drafted reply, sources used, escalation reason and internal note (escalations only), and CSAT if recorded. status is processing, completed or failed; resolution_type is resolved, escalated or pending (held for agent review), or null until completed.
idstringpathrequiredTicket ID returned from POST /tickets/resolve (tkt_…) or listed by GET /tickets (tkt_… or log_… for help-desk tickets)/ticketsList resolved ticketsscope: readPaginated list of all tickets processed by TR-AI — from your help desk and from the API — newest first, with filtering by date range, resolution type, and confidence band. Help-desk tickets have ids of the form log_<n>, source set to the help desk, and recommended_action null. API tickets still processing (or failed) are not listed; fetch them by id.
fromISO 8601 datequeryoptionalStart of date rangetoISO 8601 datequeryoptionalEnd of date range (a date-only value includes that day)resolution_typestringqueryoptionalresolved | escalated | pendingmin_confidencenumberqueryoptionalLowest confidence score to include (0–1)max_confidencenumberqueryoptionalHighest confidence score to include (0–1)pageintegerqueryoptionalPage number (default: 1)per_pageintegerqueryoptionalResults per page (default: 25, max: 100)/tickets/{id}/feedbackRecord customer satisfaction (CSAT)scope: writeRecord the customer's 1–5 satisfaction rating for a processed ticket. One rating per ticket: submitting again replaces it. Ratings feed the CSAT figures in /analytics. Returns 409 ticket_not_completed while the ticket is still processing.
idstringpathrequiredTicket ID (tkt_… or log_…)ratingintegerbodyrequired1 (very dissatisfied) to 5 (very satisfied)commentstringbodyoptionalFree-text comment (max 2,000 characters){
"object": "feedback",
"ticket_id": "tkt_5b1e0c9a7d2f4e8a91c3b6d0",
"rating": 5,
"comment": "Fixed it, thanks!",
"submitted_at": "2026-11-05T10:02:44.000Z"
}Analytics
/analytics/summaryGet automation summary metricsscope: readReturns ticket counts, automation rate (resolved ÷ total), average confidence score, average resolution time, CSAT breakdown (from ratings recorded with POST /tickets/{id}/feedback) and estimated time savings (8 agent-minutes per AI-resolved ticket) for a given period. Rolling periods end now.
periodstringqueryoptional7d | 30d | 90d | custom (default: 30d)fromISO 8601 datequeryoptionalRequired when period=customtoISO 8601 datequeryoptionalRequired when period=custom (range up to 366 days){
"object": "analytics_summary",
"period": "30d",
"from": "2026-10-06T09:30:00.000Z",
"to": "2026-11-05T09:30:00.000Z",
"tickets": {
"total": 1240,
"resolved": 868,
"escalated": 310,
"pending": 62
},
"automation_rate": 0.7,
"avg_confidence_score": 0.842,
"avg_resolution_time_ms": 4210,
"csat": {
"responses": 212,
"average": 4.41,
"satisfaction_rate": 0.877,
"breakdown": {
"1": 6,
"2": 8,
"3": 12,
"4": 58,
"5": 128
}
},
"time_saved": {
"hours": 115.7,
"minutes_per_resolved_ticket": 8
}
}/analytics/trendsGet daily metric trendsscope: readDaily time-series data (UTC days) for automation rate, ticket volume, CSAT score or average resolution time (ms). Days without data have value null (0 for ticket_volume). Useful for building custom dashboards.
metricstringqueryrequiredautomation_rate | ticket_volume | csat | resolution_timefromISO 8601 datequeryrequiredStart datetoISO 8601 datequeryrequiredEnd date (inclusive; range up to 366 days){
"object": "analytics_trend",
"metric": "automation_rate",
"unit": "ratio",
"from": "2026-11-01T00:00:00.000Z",
"to": "2026-11-04T00:00:00.000Z",
"data": [
{
"date": "2026-11-01",
"value": 0.68
},
{
"date": "2026-11-02",
"value": 0.72
},
{
"date": "2026-11-03",
"value": null
}
]
}Knowledge Base
/knowledge/articlesAdd a knowledge base articlescope: kbPush a new article directly to TR-AI's knowledge index. It is chunked, embedded and indexed before the response returns (status "indexed"), so the next ticket can use it. If indexing fails the article is saved with status "pending" and indexing is retried automatically on your next ticket or knowledge base request (at most once a minute). Articles pushed through the API survive knowledge base re-crawls: they are re-indexed the same way after a re-crawl.
titlestringbodyrequiredArticle title (max 500 characters)bodystringbodyrequiredArticle body in plain text or Markdown ("## " headings become sections; max 200,000 characters)tagsstring[]bodyoptionalCategory tags (max 20), added to the indexed text so tickets using those terms match the articleurlstringbodyoptionalPublic URL of the article, cited as its source{
"id": "art_0f6c2b8e41d9a7c35e2b1f90",
"object": "article",
"title": "Refund policy",
"tags": [
"billing"
],
"url": "https://help.example.com/refunds",
"status": "indexed",
"chunk_count": 2,
"created_at": "2026-11-05T09:41:00.000Z",
"updated_at": "2026-11-05T09:41:00.000Z"
}/knowledge/articlesList knowledge base articlesscope: readPaginated list of the articles you pushed through the API (crawled help-center pages are not listed; use /knowledge/search to query everything).
pageintegerqueryoptionalPage number (default: 1)per_pageintegerqueryoptionalResults per page (default: 25, max: 100)/knowledge/articles/{id}Get a knowledge base articlescope: readReturns the article, including its body.
idstringpathrequiredArticle ID/knowledge/articles/{id}Update a knowledge base articlescope: kbReplace an article's title, body, tags and url (send them all; omitted tags and url are cleared). The article is re-indexed before the response returns.
idstringpathrequiredArticle IDtitlestringbodyrequiredArticle titlebodystringbodyrequiredArticle body in plain text or Markdowntagsstring[]bodyoptionalCategory tagsurlstringbodyoptionalPublic URL of the article/knowledge/articles/{id}Remove a knowledge base articlescope: kbRemove an article from the knowledge index. Takes effect immediately.
idstringpathrequiredArticle ID{
"id": "art_0f6c2b8e41d9a7c35e2b1f90",
"object": "article",
"deleted": true
}/knowledge/searchSearch the knowledge basescope: readSemantic search over everything the AI answers from — crawled help-center pages and API articles — returning the best-matching passages.
qstringqueryrequiredSearch text (max 2,000 characters)limitintegerqueryoptionalNumber of results (default: 5, max: 20){
"object": "list",
"query": "how do refunds work",
"data": [
{
"object": "search_result",
"chunk_id": 48213,
"article_id": "art_0f6c2b8e41d9a7c35e2b1f90",
"source_url": "https://help.example.com/refunds",
"similarity": 0.81,
"content": "[Refund policy] Refunds are available within 30 days…"
}
]
}Account
/meInspect the current API keyscope: anyReturns the key's name, prefix, scopes, trial account and rate limits. Handy for checking a key works.
{
"object": "api_key",
"name": "Production backend",
"prefix": "tr_k3j9x2ab7q",
"scopes": [
"read",
"resolve"
],
"trial_id": 1042,
"rate_limits": {
"default": 120,
"resolve": 30
}
}Known issues
/known-issuesList known issuesscope: readKnown issues your team is working on (status=active, the default), all of them (status=all) or one stage (investigating, identified, fix_in_progress, monitoring, resolved, wont_fix). Internal notes, message templates and affected customers are never returned.
statusstringqueryoptionalactive (default), all, or a status.{
"object": "list",
"data": [
{
"object": "known_issue",
"id": 12,
"title": "CSV export fails with ERR_EXPORT_504",
"description": "Large CSV exports time out.",
"status": "fix_in_progress",
"severity": "high",
"category": "troubleshooting",
"product_area": "Reports",
"workaround": "Export one month at a time.",
"eta": "Friday",
"keywords": [
"ERR_EXPORT_504",
"csv export"
],
"examples": [
"My export never finishes"
],
"matching_enabled": true,
"public": false,
"affected_count": 37,
"created_at": "2026-11-02T09:12:00.000Z",
"updated_at": "2026-11-03T15:40:00.000Z",
"resolved_at": null
}
]
}/known-issues/{id}Get a known issuescope: readOne known issue by id.
idintegerpathrequiredKnown issue id.{
"object": "known_issue",
"id": 12,
"title": "CSV export fails with ERR_EXPORT_504",
"description": "Large CSV exports time out.",
"status": "fix_in_progress",
"severity": "high",
"category": "troubleshooting",
"product_area": "Reports",
"workaround": "Export one month at a time.",
"eta": "Friday",
"keywords": [
"ERR_EXPORT_504",
"csv export"
],
"examples": [
"My export never finishes"
],
"matching_enabled": true,
"public": false,
"affected_count": 37,
"created_at": "2026-11-02T09:12:00.000Z",
"updated_at": "2026-11-03T15:40:00.000Z",
"resolved_at": null
}/known-issuesCreate a known issuescope: writeCreates a known issue (e.g. from your incident tooling). Matching tickets get the stage message instead of an AI answer, are linked as affected and are updated automatically when the issue is resolved. Give a description, examples or keywords so tickets can match. Message templates are drafted by AI in your brand voice; edit them in the dashboard.
titlestringbodyrequiredShort title.descriptionstringbodyoptionalWhat customers are told the problem is.examplesstring[]bodyoptionalHow customers describe it.keywordsstring[]bodyoptionalKeywords and error codes (an error code match is decisive).severitystringbodyoptionallow, medium (default), high, critical.workaroundstringbodyoptionalShared with affected customers.etastringbodyoptionalExpected fix, e.g. "Friday".{
"object": "known_issue",
"id": 12,
"title": "CSV export fails with ERR_EXPORT_504",
"description": "Large CSV exports time out.",
"status": "fix_in_progress",
"severity": "high",
"category": "troubleshooting",
"product_area": "Reports",
"workaround": "Export one month at a time.",
"eta": "Friday",
"keywords": [
"ERR_EXPORT_504",
"csv export"
],
"examples": [
"My export never finishes"
],
"matching_enabled": true,
"public": false,
"affected_count": 37,
"created_at": "2026-11-02T09:12:00.000Z",
"updated_at": "2026-11-03T15:40:00.000Z",
"resolved_at": null
}/known-issues/{id}Update a known issuescope: writeEdit fields or move the stage with status. Setting status to resolved or wont_fix sends the final message to every affected customer (once each, rate limited, retried on failure). For other stages, pass notify: true to update affected customers too.
statusstringbodyoptionalNew stage.notifybooleanbodyoptionalSend the stage message to affected customers (non-final stages).{
"object": "known_issue",
"id": 12,
"title": "CSV export fails with ERR_EXPORT_504",
"description": "Large CSV exports time out.",
"status": "fix_in_progress",
"severity": "high",
"category": "troubleshooting",
"product_area": "Reports",
"workaround": "Export one month at a time.",
"eta": "Friday",
"keywords": [
"ERR_EXPORT_504",
"csv export"
],
"examples": [
"My export never finishes"
],
"matching_enabled": true,
"public": false,
"affected_count": 37,
"created_at": "2026-11-02T09:12:00.000Z",
"updated_at": "2026-11-03T15:40:00.000Z",
"resolved_at": null
}Webhooks (custom connectors)
On the Growth Pro plan you can register HTTPS endpoints under Settings → Webhooks and choose which events they receive. Each event is POSTed as JSON: { "id": "evt_…", "type": "ticket.resolved", "created_at": "…", "trial_id": 1042, "data": { … } }. Ticket events carry the same ticket object the API returns. Headers: X-TR-Event, X-TR-Event-Id, X-TR-Delivery-Id and X-TR-Signature. Respond with any 2xx within 10 seconds; redirects are not followed. Failed deliveries are retried 5 more times — after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours — and every attempt is listed under Recent deliveries, where you can also send a test ("ping") event.
ticket.resolvedA ticket was resolved by the AI (reply ready or sent).ticket.triagedA ticket was escalated to a human.ticket.review_pendingA drafted reply is waiting for agent review.review.approvedAn agent approved a drafted reply.review.rejectedAn agent rejected a drafted reply.delivery.failedPosting a reply to the help desk failed.kb.article.indexedA knowledge base article pushed through the API was indexed.kb.article.deletedA knowledge base article pushed through the API was removed.known_issue.createdA known issue was created.known_issue.updatedA known issue changed (status, details or matching).known_issue.resolvedA known issue was resolved; affected customers are being updated.Verifying signatures
X-TR-Signature is "t=<unix seconds>,v1=<hex>", where v1 is the HMAC-SHA256 of "<t>.<raw request body>" keyed with the endpoint's signing secret (whsec_…). Recompute it over the raw body, compare in constant time, and reject timestamps older than 5 minutes.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return parts.v1?.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}Need an API key?
API access is included on every plan (60, 120 or 600 requests per minute). Webhooks (custom connectors) are part of Growth Pro.
View pricing