API reference

API Documentation

Browse endpoints by category.

Overview

How to read and use these files.

The values you set

PlaceholderMeaningExample
{{base_url}}API host + porthttp://localhost:4000
{{token}}Cognito token (ID/access token of the logged-in user)<token>
{{org_id}}Active org id — sent as the X-Org-Id header on org-scoped routes<org_id>

Every * - curl.md file uses these placeholders and nothing else machine-specific. Set them once, paste, run. ({{org_id}} only appears on org-scoped routes — see Conventions below.)

In Postman

  1. Create an Environment (e.g. Rai local).
  2. Add three variables: base_url, token, and org_id.
  3. Import a curl snippet: Import → Raw text → paste the curl block → Send.
  4. Postman fills {{base_url}}, {{token}}, and {{org_id}} from the active environment.

In a terminal

export BASE_URL=http://localhost:4000
export TOKEN=<token>        # paste a real Cognito token
export ORG_ID=<org_id>      # the org you're acting in (org-scoped routes)

Then in the curl, swap {{base_url}}$BASE_URL, {{token}}$TOKEN, and {{org_id}}$ORG_ID.

Where the token comes from

v2 auth is AWS Cognito. Log in through the frontend, then copy the access token (browser devtools → Application/Storage, or the Network tab on a /me call). Paste it as token.

Conventions used everywhere

  • Response envelope — every response is { "success": bool, "data": ..., "error": "" }.
  • Org context — org-scoped routes need an X-Org-Id: {{org_id}} header naming the org you're acting in; it's re-validated against your membership on every call. Routes like health, auth, and /me (profile) need no org; orgs/* and invites/* take the org from the URL path instead.
  • Auth header — protected routes need Authorization: Bearer {{token}}. Files note when a route is public.

HTTP status codes

CodeMeaning
200Success
201Created
400Bad request / validation error
401Not authenticated (missing/invalid token)
403Authenticated but not allowed (role / access / archived)
404Resource not found
409Conflict (e.g. slug taken, sprint already active)
410Gone (invite used / revoked / expired)
429Rate limit exceeded
500Server error
502Upstream failure (e.g. SES email send)

Rate limiting

1500 requests per 15 minutes per IP (~100/min). Over the limit returns 429:

{ "success": false, "data": {}, "error": "Too many requests - please try again in 15 minutes" }

Other endpoints

GET/api/health

Health Check

GET /api/health

Checks the API and database are up. No auth required.

Request

  • No auth, no params.

Responses

200 — OK

{
  "success": true,
  "data": { "status": "ok", "db": "connected", "timestamp": "2026-06-02T14:00:00.000Z" },
  "error": ""
}

db is "disconnected" if the DB ping fails but the server still answers.

500 — server/DB error

{ "success": false, "data": {}, "error": "Internal server error" }

Sample curl

curl {{base_url}}/api/health

No token needed — this route is public.

GET/api/v1/search

Search Tickets

GET /api/v1/search

Org-scoped, visibility-aware ticket search by title or key (case-insensitive). Key matches rank first, then most-recently-updated. Capped at 20 results.

Request

  • Auth required. Org-scoped — needs the X-Org-Id header.
  • Query:
    • q — search term (required, min 2 characters). Owners/admins search every non-archived project in the org; others search public projects plus their own.

Responses

200 — array of matches (max 20)

{
  "success": true,
  "data": [
    {
      "id": "12",
      "key": "PROJ-12",
      "title": "Fix login redirect loop",
      "type": "bug",
      "status": "in_progress",
      "priority": "high",
      "assignee": {
        "id": "a1b2...",
        "name": "Jane Doe",
        "email": "jane@example.com",
        "role_type": "user",
        "role_name": "Member",
        "initials": "JD"
      },
      "projectId": "7",
      "projectName": "Rai Web",
      "projectKey": "PROJ"
    }
  ],
  "error": ""
}

assignee is null when the ticket is unassigned.

400 — query too short (fewer than 2 characters)

{ "success": false, "data": [], "error": "Query must be at least 2 characters" }

401 — no or invalid token

{ "success": false, "data": {}, "error": "Authentication required" }

403 — no active org context

{ "success": false, "data": {}, "error": "No organization selected - switch into an org and retry" }

500 — db error

{ "success": false, "data": [], "error": "Search failed" }

Sample curl

curl "{{base_url}}/api/v1/search?q=login" \
  -H "Authorization: Bearer {{token}}" \
  -H "X-Org-Id: {{org_id}}"

q must be at least 2 characters. Org context comes from the X-Org-Id header.

GET/api/v1/users/:id/tickets

Get User Tickets

GET /api/v1/users/:id/tickets

Returns every ticket assigned to one user in the current org, filtered by what the caller can see. Owners/admins see org-wide; everyone else only sees public projects plus projects they belong to.

Request

  • Auth required. Org-scoped — needs the X-Org-Id header.
  • Path: :id — target user id (the assignee whose tickets you want)
  • No query params.

Responses

200 — array of work items (each item is { ticket, project, sprint })

{
  "success": true,
  "data": [
    {
      "ticket": {
        "id": "12",
        "key": "PROJ-12",
        "title": "Fix login redirect loop",
        "description": "Users bounce between /login and /home",
        "type": "bug",
        "status": "in_progress",
        "priority": "high",
        "assignee": null,
        "reporter": null,
        "parentKey": null,
        "subTicketKeys": [],
        "comments": [],
        "sprintId": "3",
        "projectId": "7",
        "createdAt": "2026-06-01T10:00:00.000Z",
        "updatedAt": "2026-06-02T09:30:00.000Z",
        "assigneeName": "Jane Doe",
        "reporterName": "John Smith"
      },
      "project": {
        "id": "7",
        "name": "Rai Web",
        "key": "PROJ",
        "description": null,
        "archived": false,
        "visibility": "public",
        "ticketCount": 0,
        "memberCount": 0,
        "activeSprintName": null,
        "lastActive": "2026-06-02T09:30:00.000Z"
      },
      "sprint": {
        "id": "3",
        "name": "Sprint 2",
        "status": "active",
        "startDate": "2026-05-26T00:00:00.000Z",
        "endDate": "2026-06-09T00:00:00.000Z"
      }
    }
  ],
  "error": ""
}

When a ticket is in the backlog (no sprint), ticket.sprintId is null and sprint is { "id": "", "name": "Backlog", "status": "planned", "startDate": "", "endDate": "" }.

400 — missing/invalid user id

{ "success": false, "data": {}, "error": "Invalid user id" }

401 — no or invalid token

{ "success": false, "data": {}, "error": "Authentication required" }

403 — no active org context

{ "success": false, "data": {}, "error": "No organization selected - switch into an org and retry" }

404 — target user not a member of this org

{ "success": false, "data": {}, "error": "User not found in this organization" }

500 — db error

{ "success": false, "data": {}, "error": "Could not load user tickets" }

Sample curl

curl {{base_url}}/api/v1/users/USER_ID/tickets \
  -H "Authorization: Bearer {{token}}" \
  -H "X-Org-Id: {{org_id}}"

Swap USER_ID for the target user's id. Org context comes from the X-Org-Id header.