Public API v1
REST API v1 reference for reading and managing QA Note projects and issues from external systems.
Table of Contents
- Versioning policy
- Authentication
- Base URL · Request format
- Endpoints (v1)
- `GET /api/v1/projects`
- `GET /api/v1/projects/{projectId}/issues`
- `GET /api/v1/projects/{projectId}/issues/{issueNumber}`
- `PATCH /api/v1/projects/{projectId}/issues/{issueNumber}`
- `POST /api/v1/projects/{projectId}/issues/{issueNumber}/comments`
- Error responses
- Rate limits
- Webhook vs API
- Changelog
- SDK · code samples
- curl
- JavaScript (`fetch`)
- Node.js (with error handling)
Public API v1
The QA Note Public API v1 is a REST API for working with projects and issues from external systems. Every path is prefixed with /api/v1/, and breaking changes only ship under a new version (v2).
Internal endpoints used by the Extension and Widget (for example /api/extension/...) are out of scope for this document. See the Endpoint Reference for the full list and the Authentication page for issuing API keys.
Versioning policy
- Stability guarantee: v1 response schemas and paths stay stable as long as the change is backward-compatible. Adding a new field is considered backward-compatible.
- Breaking changes: removing a field, changing a type, or moving a path ships only with v2. v1 is supported in parallel for at least six months after v2 releases.
- Change announcements: backward-incompatible changes are pre-announced in the Changelog and via dashboard notices.
Authentication
Public API v1 accepts an API Key or an MCP OAuth access token. API keys are recommended for CI/CD, headless servers, and external automation.
curl https://qanote.app/api/v1/projects \
-H "Authorization: Bearer qn_YOUR_API_KEY"
| Item | Value |
|---|---|
| Header | Authorization: Bearer qn_... |
| Scope (read) | projects:read · issues:read |
| Scope (write) | issues:write |
| Issued from | Dashboard → Org settings → API Keys |
Each API key is bound to a single organization. Issue a separate key per organization if you need to access multiple. See the Authentication guide for details.
Base URL · Request format
https://qanote.app/api/v1
- All requests and responses are UTF-8 JSON.
POSTandPATCHrequests must sendContent-Type: application/json.- Timestamps are ISO 8601 (
2025-03-15T14:30:00.000Z).
Endpoints (v1)
The full endpoint catalog lives in the Endpoint Reference. The endpoints below are the ones used most often from external integrations.
GET /api/v1/projects
Returns the projects in the organization the API key was issued for.
Required scope: projects:read
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
organizationSlug | string | No | Filter by organization slug (the slug in /dashboard/<slug>/...) |
organizationId | string (uuid) | No | Filter by organization id (alternative to organizationSlug) |
Request
curl https://qanote.app/api/v1/projects \
-H "Authorization: Bearer qn_YOUR_API_KEY"
Response
{
"data": [
{
"id": "project_abc123",
"name": "Web Service",
"slug": "web-service",
"description": null,
"organizationId": "2a3f7b04-6e91-4c2d-9f77-3d6c9a1e4b58",
"organizationSlug": "acme",
"organizationName": "Acme Inc.",
"createdAt": "2025-01-15T09:00:00.000Z"
}
]
}
GET /api/v1/projects/{projectId}/issues
Lists issues in a project with pagination and filtering. The response keeps the body light (LLM token-friendly) and exposes only the core fields.
Required scope: issues:read
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
status | string | No | open · in_progress · resolved · closed |
priority | string | No | low · medium · high · critical |
q | string | No | Search across title and description |
label | string | No | Filter by label name |
page | number | No | Page number (default 1) |
limit | number | No | Items per page (default 20, max 100) |
Request
curl "https://qanote.app/api/v1/projects/PROJECT_ID/issues?status=open&limit=20" \
-H "Authorization: Bearer qn_YOUR_API_KEY"
Response
{
"data": [
{
"id": "issue_xyz",
"issueNumber": 42,
"title": "TypeError raised when submitting password on /login",
"status": "open",
"priority": "high",
"labels": ["bug", "auth"],
"assignees": ["Jee Lee"],
"createdAt": "2025-03-15T14:30:00.000Z",
"updatedAt": "2025-03-15T14:30:00.000Z"
}
],
"total": 42,
"page": 1,
"totalPages": 3
}
GET /api/v1/projects/{projectId}/issues/{issueNumber}
Fetches a single issue. The response includes a metadata summary (console error count, network error count, JS error count). Detailed metadata bodies live behind dedicated endpoints (console-logs, network-logs, etc.).
Required scope: issues:read
curl https://qanote.app/api/v1/projects/PROJECT_ID/issues/42 \
-H "Authorization: Bearer qn_YOUR_API_KEY"
PATCH /api/v1/projects/{projectId}/issues/{issueNumber}
Updates the status or priority of an issue. Useful for external automation — e.g. a GitHub Actions workflow that flips an issue to resolved on merge.
Required scope: issues:write
Request body
| Name | Type | Required | Description |
|---|---|---|---|
status | string | No | New status |
priority | string | No | New priority |
curl -X PATCH https://qanote.app/api/v1/projects/PROJECT_ID/issues/42 \
-H "Authorization: Bearer qn_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "in_progress"}'
POST /api/v1/projects/{projectId}/issues/{issueNumber}/comments
Adds a comment to an issue. Useful for external bots — e.g. a deploy notifier that comments on the related issue.
Required scope: issues:write
curl -X POST https://qanote.app/api/v1/projects/PROJECT_ID/issues/42/comments \
-H "Authorization: Bearer qn_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Reproduced this on the Preview deployment."}'
Creating new issues is not part of Public API v1. Issues authored through the Extension, Widget, or dashboard can be read and updated through this API.
Error responses
All errors return a standard HTTP status code and a JSON body. The body shape and code-by-code meaning are documented in Error Handling.
{
"error": "Insufficient scope"
}
Validation failures include a details array.
{
"error": "Invalid input",
"details": [
{ "field": "status", "message": "Invalid enum value." }
]
}
| Status | Meaning |
|---|---|
200 · 201 | Success |
400 | Bad request body or query |
401 | Authentication failed (missing or expired API key) |
403 | Insufficient scope |
404 | Resource not found or belongs to another organization |
429 | Rate limit exceeded |
500 | Internal server error |
Rate limits
| Limit | Value |
|---|---|
| Per minute | 100 req/min per API key (sliding window) |
| Auth attempts | 5 per 15 minutes |
When the limit is exceeded the API returns 429 Too Many Requests. Honor the Retry-After (seconds) header or apply exponential backoff.
# Example response headers
HTTP/2 429
Retry-After: 32
Webhook vs API
| Use case | Recommended |
|---|---|
| Receive events (issue created, status changed, …) | Webhook |
| Active queries · status updates · comments | Public API v1 |
| Calls from AI coding tools | MCP Server (OAuth) |
Webhooks are a one-way push from QA Note to your system. Use this API when your system needs to pull data or push changes on its own.
Changelog
| Version | Date | Changes |
|---|---|---|
v1.0 | 2026-04 | Initial public release — /projects, /issues, /comments, plus 13 metadata endpoints |
Backward-incompatible changes are added to this table as they ship.
SDK · code samples
curl
curl https://qanote.app/api/v1/projects \
-H "Authorization: Bearer qn_YOUR_API_KEY"
JavaScript (fetch)
const res = await fetch("https://qanote.app/api/v1/projects", {
headers: { Authorization: `Bearer ${process.env.QANOTE_API_KEY}` },
});
const { data } = await res.json();
Node.js (with error handling)
async function listOpenIssues(projectId: string) {
const res = await fetch(
`https://qanote.app/api/v1/projects/${projectId}/issues?status=open`,
{ headers: { Authorization: `Bearer ${process.env.QANOTE_API_KEY}` } },
);
if (res.status === 429) {
const retryAfter = Number(res.headers.get("retry-after") ?? 30);
throw new Error(`Rate limited. Retry after ${retryAfter}s`);
}
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error ?? `HTTP ${res.status}`);
}
const { data } = await res.json();
return data;
}
The complete endpoint specification continues in the Endpoint Reference.