API Documentation

Everything you need to integrate with the Trajan Public Ticket API — authentication, endpoints, rate limits, and code examples.

https://api.trajancloud.com

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.

Create ticket — cURL
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

Response
{
  "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.

List tickets — cURL
400">curl https://api.trajancloud.com/api/v1/public/tickets/ \
  -H "Authorization: Bearer trj_pk_your_api_key"

Response 200

Response
{
  "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.

Header format
Authorization: Bearer trj_pk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Scopes

Each API key is granted one or more scopes that control which endpoints it can access.

tickets:readscopeOptional

List tickets and retrieve individual ticket details.

tickets:writescopeOptional

Create tickets and use the interpret endpoint.

Scope Requirements by Endpoint

POST /tickets/tickets:write
POST /tickets/interprettickets:write
GET /tickets/tickets:read
GET /tickets/{id}tickets:read

Key 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 401 immediately.
  • last_used_at is 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.

POST/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

titlestringRequired

Ticket title (max 500 chars)

descriptionstringOptional

Detailed description (max 50,000 chars)

typestringOptional

bug | feature | fix | refactor | investigation | task | questionDefault: task

prioritystringOptional

critical | high | medium | low → maps to 1 | 2 | 3 | 4Default: medium

reporter_emailstringOptional

Email of the person reporting

reporter_namestringOptional

Name of the reporter

tagsstring[]Optional

Array of tag strings

metadataobjectOptional

Arbitrary JSON object for custom data

Example

Create ticket — cURL
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

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

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

201Ticket created successfully

Ticket object with id, title, status, type, priority, created_at

200Duplicate detected (trigram similarity ≥ 0.6)

duplicate: true, existing_ticket_id, existing_ticket_title, existing_ticket_status

401Missing or invalid API key

detail: error message

403API key lacks tickets:write scope

detail: error message

422Invalid type/priority or oversized fields

detail: error message

429Rate limit exceeded

detail: error message + Retry-After header

POST/api/v1/public/tickets/interpret

Submit 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

messagestringRequired

Raw text to interpret (max 50,000 chars)

titlestringOptional

Optional title override — skips AI title extraction

reporter_emailstringOptional

Email of the person reporting

reporter_namestringOptional

Name of the reporter

metadataobjectOptional

Arbitrary JSON object for custom data (max 50 keys)

Example

Interpret — cURL
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)

201 Created
{
  "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

201Ticket created successfully

Ticket object with id, title, status, type, priority, created_at

200Duplicate detected (trigram similarity ≥ 0.6)

duplicate: true, existing_ticket_id, existing_ticket_title, existing_ticket_status

401Missing or invalid API key

detail: error message

403API key lacks tickets:write scope

detail: error message

422Invalid type/priority or oversized fields

detail: error message

429Rate limit exceeded

detail: error message + Retry-After header

GET/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

statusstringOptional

Filter by status: reported | in_progress | completed

typestringOptional

Filter by type: bug | feature | fix | refactor | investigation | task | question

sourcestringOptional

Filter by source (e.g. api, agent, manual)

qstringOptional

Search ticket titles (case-insensitive)

limitintegerOptional

Results per page (1–100)Default: 50

offsetintegerOptional

Number of results to skip (≥ 0)Default: 0

Example

List tickets — cURL
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

200 OK
{
  "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

200Success

Ticket object or paginated list

401Missing or invalid API key

detail: error message

403API key lacks tickets:read scope

detail: error message

404Ticket not found or wrong product

detail: error message

429Rate limit exceeded

detail: error message + Retry-After header

GET/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

ticket_idUUIDRequired

The ticket's unique identifier

Example

Get ticket — cURL
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

200 OK
{
  "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

200Success

Ticket object or paginated list

401Missing or invalid API key

detail: error message

403API key lacks tickets:read scope

detail: error message

404Ticket not found or wrong product

detail: error message

429Rate limit exceeded

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.

Write120 req / 60 sec

POST /tickets/, POST /tickets/interpret

Interpret30 req / 60 sec

POST /tickets/interpret

Read120 req / 60 sec

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-After duration, 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

Error response
{
  "detail": "Human-readable error message"
}

Status Code Reference

200OK / Duplicate detected

Successful read, or duplicate ticket found

201Created

New ticket created successfully

401Unauthorized

Missing or invalid API key

403Forbidden

API key lacks required scope

404Not Found

Ticket doesn't exist or belongs to another project

422Validation Error

Invalid field values or oversized input

429Too Many Requests

Rate limit exceeded — check Retry-After header

Retry Strategy

401 / 403
Do not retryFix credentials or scopes
404
Do not retryVerify the ticket ID
422
Do not retryFix the request payload
429
RetryWait Retry-After seconds, then exponential backoff
5xx
RetryExponential backoff (max 3 retries)

Example: Retry with Backoff

Retry helper — JavaScript
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.

Planned

JavaScript Client

Typed wrapper around the REST API with built-in retry logic, rate-limit handling, and TypeScript definitions.

Planned

Want to be notified when SDKs ship?

Star the repo on GitHub