API Documentation
Browse endpoints by category.
Overview
How to read and use these files.
The values you set
| Placeholder | Meaning | Example |
|---|---|---|
{{base_url}} | API host + port | http://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
- Create an Environment (e.g. Rai local).
- Add three variables:
base_url,token, andorg_id. - Import a curl snippet: Import → Raw text → paste the curl block → Send.
- 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/*andinvites/*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
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad request / validation error |
| 401 | Not authenticated (missing/invalid token) |
| 403 | Authenticated but not allowed (role / access / archived) |
| 404 | Resource not found |
| 409 | Conflict (e.g. slug taken, sprint already active) |
| 410 | Gone (invite used / revoked / expired) |
| 429 | Rate limit exceeded |
| 500 | Server error |
| 502 | Upstream 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
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.
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-Idheader. - 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 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-Idheader. - 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.