API Documentation
Everything you need to integrate with the Trajan Public Ticket API — authentication, endpoints, rate limits, and code examples.
Quick Start
Submit your first ticket in under 5 minutes. Pick your language and follow along.
1Create an API Key
Open your project in Trajan, go to Settings → API Keys, and click Create Key. Give it the tickets:write and tickets:read scopes.
The key starts with trj_pk_ and is shown once — copy it immediately.
2Send Your First Ticket
POST a JSON body with at least a title field.
400">curl -X POST https://api.trajancloud.com/api/v1/public/tickets/ \
-H "Authorization: Bearer trj_pk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"title": "Login button unresponsive on mobile",
"type": "bug",
"priority": "high",
"description": "The login button does not respond to taps on iOS Safari 17."
}'Response 201
{
"id": "d4f8a1c2-9b3e-4f7a-a1d2-6c8e9f0b3a5d",
"title": "Login button unresponsive on mobile",
"status": "open",
"type": "bug",
"priority": 2,
"created_at": "2026-02-23T14:30:00Z"
}3Verify It Arrived
List your project's tickets to confirm the new entry.
400">curl https://api.trajancloud.com/api/v1/public/tickets/ \
-H "Authorization: Bearer trj_pk_your_api_key"Response 200
{
"items": [
{
"id": "d4f8a1c2-9b3e-4f7a-a1d2-6c8e9f0b3a5d",
"title": "Login button unresponsive on mobile",
"type": "bug",
"status": "open",
"priority": 2,
"source": "api",
"created_at": "2026-02-23T14:30:00Z",
"updated_at": "2026-02-23T14:30:00Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}Authentication
All requests to the Public Ticket API must include a valid API key in the Authorization header.
API Key Format
Keys use the prefix trj_pk_ followed by a 32-character random string. The full key is shown once at creation and stored server-side as a SHA-256 hash — it cannot be retrieved later.
Authorization: Bearer trj_pk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6Scopes
Each API key is granted one or more scopes that control which endpoints it can access.
| Name | Type | Required | Description |
|---|---|---|---|
| tickets:read | scope | Optional | List tickets and retrieve individual ticket details. |
| tickets:write | scope | Optional | Create tickets and use the interpret endpoint. |
tickets:readscopeOptionalList tickets and retrieve individual ticket details.
tickets:writescopeOptionalCreate tickets and use the interpret endpoint.
Scope Requirements by Endpoint
POST /tickets/tickets:writePOST /tickets/interprettickets:writeGET /tickets/tickets:readGET /tickets/{id}tickets:readKey Management
- Keys are project-scoped — each key belongs to a single product.
- Creating a key requires Editor or Admin role on the project.
- Keys can be revoked at any time — revoked keys return
401immediately. last_used_atis tracked for auditing (write-debounced to reduce DB load).
Security Best Practices
- Never commit API keys to source control.
- Use environment variables in production.
- Rotate keys periodically; revoke immediately if compromised.
Endpoints
All endpoints are relative to the base URL https://api.trajancloud.com. Responses use JSON.
/api/v1/public/tickets/Create a new ticket. If a similar ticket already exists (trigram similarity ≥ 0.6), returns the existing ticket instead of creating a duplicate.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
| title | string | Required | Ticket title (max 500 chars) |
| description | string | Optional | Detailed description (max 50,000 chars) |
| type | string | Optional | bug | feature | fix | refactor | investigation | task | questionDefault: task |
| priority | string | Optional | critical | high | medium | low → maps to 1 | 2 | 3 | 4Default: medium |
| reporter_email | string | Optional | Email of the person reporting |
| reporter_name | string | Optional | Name of the reporter |
| tags | string[] | Optional | Array of tag strings |
| metadata | object | Optional | Arbitrary JSON object for custom data |
titlestringRequiredTicket title (max 500 chars)
descriptionstringOptionalDetailed description (max 50,000 chars)
typestringOptionalbug | feature | fix | refactor | investigation | task | questionDefault: task
prioritystringOptionalcritical | high | medium | low → maps to 1 | 2 | 3 | 4Default: medium
reporter_emailstringOptionalEmail of the person reporting
reporter_namestringOptionalName of the reporter
tagsstring[]OptionalArray of tag strings
metadataobjectOptionalArbitrary JSON object for custom data
Example
400">curl -X POST https://api.trajancloud.com/api/v1/public/tickets/ \
-H "Authorization: Bearer trj_pk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"title": "Login button unresponsive on mobile",
"type": "bug",
"priority": "high",
"description": "The login button does not respond to taps on iOS Safari 17."
}'Response 201 — Created
{
"id": "d4f8a1c2-9b3e-4f7a-a1d2-6c8e9f0b3a5d",
"title": "Login button unresponsive on mobile",
"status": "open",
"type": "bug",
"priority": 2,
"created_at": "2026-02-23T14:30:00Z"
}Response 200 — Duplicate
{
"duplicate": true,
"existing_ticket_id": "d4f8a1c2-9b3e-4f7a-a1d2-6c8e9f0b3a5d",
"existing_ticket_title": "Login button unresponsive on mobile",
"existing_ticket_status": "open"
}Status Codes
| Status | Condition | Response |
|---|---|---|
| 201 | Ticket created successfully | Ticket object with id, title, status, type, priority, created_at |
| 200 | Duplicate detected (trigram similarity ≥ 0.6) | duplicate: true, existing_ticket_id, existing_ticket_title, existing_ticket_status |
| 401 | Missing or invalid API key | detail: error message |
| 403 | API key lacks tickets:write scope | detail: error message |
| 422 | Invalid type/priority or oversized fields | detail: error message |
| 429 | Rate limit exceeded | detail: error message + Retry-After header |
Ticket object with id, title, status, type, priority, created_at
duplicate: true, existing_ticket_id, existing_ticket_title, existing_ticket_status
detail: error message
detail: error message
detail: error message
detail: error message + Retry-After header
/api/v1/public/tickets/interpretSubmit raw text and let AI extract a structured ticket. The message is interpreted to produce a title, type, priority, description, and acceptance criteria. Stricter rate limit: 30 req/min.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
| message | string | Required | Raw text to interpret (max 50,000 chars) |
| title | string | Optional | Optional title override — skips AI title extraction |
| reporter_email | string | Optional | Email of the person reporting |
| reporter_name | string | Optional | Name of the reporter |
| metadata | object | Optional | Arbitrary JSON object for custom data (max 50 keys) |
messagestringRequiredRaw text to interpret (max 50,000 chars)
titlestringOptionalOptional title override — skips AI title extraction
reporter_emailstringOptionalEmail of the person reporting
reporter_namestringOptionalName of the reporter
metadataobjectOptionalArbitrary JSON object for custom data (max 50 keys)
Example
400">curl -X POST https://api.trajancloud.com/api/v1/public/tickets/interpret \
-H "Authorization: Bearer trj_pk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"message": "The checkout page crashes when I add more than 10 items to my cart. Happens every time on Chrome.",
"reporter_email": "jane@example.com"
}'Response 201 — includes a confidence score (0.0–1.0)
{
"id": "e5a9b2d3-8c4f-4e6b-b2e3-7d9f0a1c4b6e",
"title": "Checkout page crashes with 10+ cart items",
"status": "open",
"type": "bug",
"priority": 2,
"confidence": 0.92,
"created_at": "2026-02-23T14:35:00Z"
}Status Codes
| Status | Condition | Response |
|---|---|---|
| 201 | Ticket created successfully | Ticket object with id, title, status, type, priority, created_at |
| 200 | Duplicate detected (trigram similarity ≥ 0.6) | duplicate: true, existing_ticket_id, existing_ticket_title, existing_ticket_status |
| 401 | Missing or invalid API key | detail: error message |
| 403 | API key lacks tickets:write scope | detail: error message |
| 422 | Invalid type/priority or oversized fields | detail: error message |
| 429 | Rate limit exceeded | detail: error message + Retry-After header |
Ticket object with id, title, status, type, priority, created_at
duplicate: true, existing_ticket_id, existing_ticket_title, existing_ticket_status
detail: error message
detail: error message
detail: error message
detail: error message + Retry-After header
/api/v1/public/tickets/List tickets for the API key's project. Returns a paginated list ordered by created_at DESC. The description field is omitted — use the single-ticket endpoint for full details.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| status | string | Optional | Filter by status: reported | in_progress | completed |
| type | string | Optional | Filter by type: bug | feature | fix | refactor | investigation | task | question |
| source | string | Optional | Filter by source (e.g. api, agent, manual) |
| q | string | Optional | Search ticket titles (case-insensitive) |
| limit | integer | Optional | Results per page (1–100)Default: 50 |
| offset | integer | Optional | Number of results to skip (≥ 0)Default: 0 |
statusstringOptionalFilter by status: reported | in_progress | completed
typestringOptionalFilter by type: bug | feature | fix | refactor | investigation | task | question
sourcestringOptionalFilter by source (e.g. api, agent, manual)
qstringOptionalSearch ticket titles (case-insensitive)
limitintegerOptionalResults per page (1–100)Default: 50
offsetintegerOptionalNumber of results to skip (≥ 0)Default: 0
Example
400">curl "https://api.trajancloud.com/api/v1/public/tickets/?status=open&type=bug&limit=10" \
-H "Authorization: Bearer trj_pk_your_api_key"Response 200
{
"items": [
{
"id": "d4f8a1c2-9b3e-4f7a-a1d2-6c8e9f0b3a5d",
"title": "Login button unresponsive on mobile",
"type": "bug",
"status": "open",
"priority": 2,
"source": "api",
"created_at": "2026-02-23T14:30:00Z",
"updated_at": "2026-02-23T14:30:00Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}Status Codes
| Status | Condition | Response |
|---|---|---|
| 200 | Success | Ticket object or paginated list |
| 401 | Missing or invalid API key | detail: error message |
| 403 | API key lacks tickets:read scope | detail: error message |
| 404 | Ticket not found or wrong product | detail: error message |
| 429 | Rate limit exceeded | detail: error message + Retry-After header |
Ticket object or paginated list
detail: error message
detail: error message
detail: error message
detail: error message + Retry-After header
/api/v1/public/tickets/{ticket_id}Retrieve a single ticket by ID. Returns the full ticket including description, tags, and reporter info. Scoped to the API key's project — returns 404 for tickets belonging to other projects.
Path Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| ticket_id | UUID | Required | The ticket's unique identifier |
ticket_idUUIDRequiredThe ticket's unique identifier
Example
400">curl https://api.trajancloud.com/api/v1/public/tickets/d4f8a1c2-9b3e-4f7a-a1d2-6c8e9f0b3a5d \
-H "Authorization: Bearer trj_pk_your_api_key"Response 200
{
"id": "d4f8a1c2-9b3e-4f7a-a1d2-6c8e9f0b3a5d",
"title": "Login button unresponsive on mobile",
"description": "The login button does not respond to taps on iOS Safari 17.",
"status": "open",
"type": "bug",
"priority": 2,
"source": "api",
"tags": [],
"reporter_email": "dev@example.com",
"reporter_name": null,
"created_at": "2026-02-23T14:30:00Z",
"updated_at": "2026-02-23T14:30:00Z"
}Status Codes
| Status | Condition | Response |
|---|---|---|
| 200 | Success | Ticket object or paginated list |
| 401 | Missing or invalid API key | detail: error message |
| 403 | API key lacks tickets:read scope | detail: error message |
| 404 | Ticket not found or wrong product | detail: error message |
| 429 | Rate limit exceeded | detail: error message + Retry-After header |
Ticket object or paginated list
detail: error message
detail: error message
detail: error message
detail: error message + Retry-After header
Rate Limits
Rate limits use a sliding window algorithm (not fixed-window). When a limit is exceeded the API returns 429 with a Retry-After header indicating seconds to wait.
POST /tickets/, POST /tickets/interpret
POST /tickets/interpret
GET /tickets/, GET /tickets/{id}
Note: The interpret endpoint is subject to both the write limit (120/min) and the interpret limit (30/min). The stricter limit applies first.
Best Practices
- Implement exponential backoff when you receive a 429 — wait the
Retry-Afterduration, then double the delay on each subsequent retry. - Cache read responses client-side to reduce unnecessary requests to the list and get endpoints.
- Batch where possible — avoid submitting duplicate tickets by checking the list endpoint first or relying on the built-in duplicate detection.
Error Handling
All errors return a JSON body with a detail field containing a human-readable message.
Standard Error Format
{
"detail": "Human-readable error message"
}Status Code Reference
| Status | Condition | Response |
|---|---|---|
| 200 | OK / Duplicate detected | Successful read, or duplicate ticket found |
| 201 | Created | New ticket created successfully |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | API key lacks required scope |
| 404 | Not Found | Ticket doesn't exist or belongs to another project |
| 422 | Validation Error | Invalid field values or oversized input |
| 429 | Too Many Requests | Rate limit exceeded — check Retry-After header |
Successful read, or duplicate ticket found
New ticket created successfully
Missing or invalid API key
API key lacks required scope
Ticket doesn't exist or belongs to another project
Invalid field values or oversized input
Rate limit exceeded — check Retry-After header
Retry Strategy
Example: Retry with Backoff
400">async 400">function submitWithRetry(url, body, apiKey, maxRetries = 3) {
for (400">let attempt = 0; attempt <= maxRetries; attempt++) {
400">const response = 400">await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
400">if (response.ok) 400">return response.json();
400">if (response.status === 429 || response.status >= 500) {
400">const retryAfter = response.headers.get("Retry-After");
400">const delay = retryAfter
? Number(retryAfter) * 1000
: Math.min(1000 * 2 ** attempt, 30000);
400">await 400">new Promise((r) => setTimeout(r, delay));
continue;
}
// 401, 403, 404, 422 — do not retry
400">const error = 400">await response.json();
400">throw 400">new Error(error.detail);
}
400">throw 400">new Error("Max retries exceeded");
}SDKs & Integrations
Official client libraries are on the roadmap. In the meantime the REST API works with any HTTP client.
React Feedback Widget
Drop-in <TrajanFeedback /> component — embed a ticket form in your app with one line of code.
JavaScript Client
Typed wrapper around the REST API with built-in retry logic, rate-limit handling, and TypeScript definitions.
PlannedWant to be notified when SDKs ship?
Star the repo on GitHub