Build on the GoodQA API.
A typed REST API and an MCP server for agents. File issues, generate test plans, run missions, and export to GitHub — all programmatically, with the same tenancy and RBAC guarantees as the app.
Create an API key
API keys authenticate agents and the MCP server. Generate one from Settings → API keys in your organization. Keys look like bt_<prefix>_<secret> and carry independent read/write and per-resource scopes. Pass them in the Authorization header — the same header a bearer JWT uses. An issue created with an API key is automatically recorded as agent/API-created with that key's id; a user session (JWT or browser cookie) is recorded as manual. These fields are server-controlled and cannot be supplied in an issue request.
Issue provenance is automatic. A write made with an API key is recorded as an agent/API-created issue with that key's attribution; a browser or other user session is recorded as human-created. The create endpoint never accepts a client-supplied source or attribution id.
Go to SettingsQuickstart
Authenticate and file an issue
Log in for a user-session token (or use an organization API key directly), then create an issue in a project. The server derives creator provenance from that credential; every write endpoint validates against the same Zod schemas the app uses.
curl -s https://goodqa.app/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"••••••••"}'
# → { "user": {...}, "tokens": { "accessToken": "ey…", "refreshToken": "…" } }curl -s https://goodqa.app/v1/projects/$PROJECT_ID/issues \
-H "Authorization: Bearer $BUGTRACKER_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"title": "Checkout button unresponsive on mobile",
"type": "bug",
"status": "triage",
"priority": "high",
"severity": "major",
"reproSteps": "1. Open /cart on iOS Safari\n2. Tap Checkout",
"expected": "Navigates to payment",
"actual": "Nothing happens"
}'Issue lists
Search and paginate without losing results
Use search for title and description search; q is an exact alias. Set limit from 1 to 100. When a response includes nextCursor, send it unchanged as cursor on the next request with every active filter repeated. A different q and search, or any offset, page, or skip returns HTTP 400 instead of silently restarting at page one. Use audience for server-derived creator/ownership slices, or createdViaApiKeyId and assignedAgentApiKeyId for an exact agent identity. Every response includes full authorised-result counts, independent of the cursor and active audience filter.
curl -s \
"https://goodqa.app/v1/projects/$PROJECT_ID/issues?search=checkout&limit=100" \
-H "Authorization: Bearer $BUGTRACKER_API_KEY"
# → { "issues": [...], "nextCursor": "eyJ...", "counts": { "total": 42, ... } }
# When nextCursor is non-null, pass it back unchanged as cursor and repeat every active filter.
curl -s --get https://goodqa.app/v1/projects/$PROJECT_ID/issues \
-H "Authorization: Bearer $BUGTRACKER_API_KEY" \
--data-urlencode 'limit=100' \
--data-urlencode 'search=checkout' \
--data-urlencode 'audience=agent_or_api_created' \
--data-urlencode "cursor=$NEXT_CURSOR"Every issue list also accepts one audience filter: human_created, agent_or_api_created, assigned_to_me, or created_by_me. Responses include counts for all five choices before the audience filter or cursor is applied, limited to the caller's authorized result set.
Agents
Model Context Protocol
Drive GoodQA from Claude, Cursor, or any MCP-compatible client — the same tenancy and permission rules as the app itself.
Remote server — recommended
Download .mcpbWorks with Claude Desktop and any client that speaks Streamable HTTP MCP. Open the downloaded .mcpb file (Claude Desktop → Settings → Extensions), then run the command below once from a terminal — it prints a short code and a link. Open the link, sign in, pick which organization to grant, and approve. No API key to copy or store yourself.
unzip the downloaded goodqa.mcpb, then run: node bundle.js pairEndpoint: /v1/mcp — authenticated the same way as any GoodQA API key, scoped to the org and role you approve.
Agents that talk to people: read the conversations skill first (bot identification, specific questions, untrusted replies, escalation) at /v1/meta/agent-skill, or over MCP as resource goodqa://skills/conversations.
Local server (repo checkout)
Running this repo directly? goodqa-mcp is a stdio server over the API, authenticated with a manually created API key — exposes tools for issues, test plans, missions, and more.
claude mcp add bugtracker \
-e BUGTRACKER_API_KEY=... \
-e BUGTRACKER_API_URL=https://goodqa.app \
-- pnpm --filter goodqa-mcp startOn by default
Capabilities switchable per organization
Every capability here is on by default for every organization, on every plan. GoodQA can switch one off for an organization; then its routes answer 404 and its MCP tools are not listed. A capability that needs infrastructure the server does not have yet (custom data needs its database cluster) answers 501 not_configured instead.
Chat questions
On by defaultAsk a multiple-choice question in chat; people answer with buttons and an optional free-text reply.
Switch:
chat.questionsMCP: chat_ask, chat_get_answers
Secret requests
On by defaultAsk a member for a password or API key; they type it into a masked form that stores it in the vault for you alone.
Switch:
vault.requests,vaultRequests.enabledMCP: request_secret, read_requested_secret
Attachment sharing
On by defaultShare links to a file (team only, anyone with the link, or named people), Forward a file into other conversations, and an org policy for public links.
Switch:
attachments.sharingMCP: attachment_share_link, attachment_list_shares, attachment_revoke_share, attachment_forward
Agent knowledge
On by defaultDurable facts, decisions and how-tos for agents, with history, search and review dates.
Switch:
agentKnowledge.enabledMCP: memory_write, memory_search, memory_list, memory_get, memory_supersede, memory_forget, memory_purge
CRM v2
On by defaultPeople and companies with flags, tags, your own external keys, idempotent upsert and batch upsert, changed-since sync, lifecycles with stage SLAs, relationships and CRM webhooks.
Switch:
crm.api.enabled,crm.v2.records,crm.v2.lifecycles,crm.v2.relationships,crm.v2.webhooksMCP: crm_* (up to 29 tools)
Custom data
On by defaultAn organization's own PostgreSQL tables behind a gated SQL API, with structured row calls, import and export. Needs a database cluster on the server: until then every call answers 501 not_configured and the tools are not listed.
Switch:
customData.enabledMCP: data_status, describe, query, find_rows, insert_rows and 7 more
Outreach
On by defaultPermit-gated contact of published business contacts: suppressions, kill switch, policy caps, templates, sequences, webhooks, and Gmail, Microsoft 365 and Zoho mailbox connectors. Bots may only make it safer.
Switch:
outreach.enabled,outreach.api.enabled,outreach.sequences,outreach.webhooks,outreach.byo,outreach.gmail,outreach.outlook,outreach.zoho,outreach.wizard,outreach.queue,outreach.leads,outreach.outlookNotificationsMCP: outreach_* (18, plus outreach_deliver_send with a mailbox connector, 4 for bring-your-own sender and 5 for the work queue)
Coordinator
On by defaultGoodQA as an agent's source of truth: one bootstrap read at session start (rules, handoff, lanes, inbox, changes) and retry-safe checkpoints. A person runs setup once.
Switch:
coordinator.bootstrapMCP: coordinator_bootstrap, coordinator_checkpoint
Integrations
Webhooks, scopes and missions
Webhooks
Register endpoints under /orgs/{orgId}/webhooks (list, create, update, test, rotate-secret, deliveries, redeliver, requeue-failed). Each request carries x-goodqa-signature: t=<unix>,v1=<hex> where v1 is HMAC-SHA256 of "<t>.<rawBody>" with your secret, plus x-goodqa-event, x-goodqa-delivery (the same id on every retry: dedupe on it) and user agent GoodQA-Webhooks/1. Reject a timestamp more than 300 s from now. While a secret is being rotated the header carries one v1= per accepted secret; accept any. Failed deliveries are retried with exponential backoff (30 s doubling, capped at 1 h) up to 6 attempts, then kept as failed and can be redelivered. Redirects are never followed.
Events:
chat.dm.message.createdchat.message.createdchat.mention.createdissue.comment.createdcrm.record.createdcrm.record.updatedcrm.record.archivedcrm.flags.changedcrm.tags.changedcrm.relationship.createdcrm.relationship.endedcrm.lifecycle.enrolledcrm.lifecycle.transitionedcrm.lifecycle.sla_breached
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the exact bytes received (do not re-serialise). secret: your whsec_... value.
export function verify(secret, headers, rawBody) {
const header = headers['x-goodqa-signature'] ?? '';
const parts = header.split(',').map((p) => p.split('='));
const t = Number(parts.find(([k]) => k === 't')?.[1]);
const candidates = parts.filter(([k]) => k === 'v1').map(([, v]) => v); // several during secret rotation
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
return candidates.some((c) => {
const got = Buffer.from(c, 'hex');
return got.length === expected.length && timingSafeEqual(got, expected);
});
}
// Dedupe on headers['x-goodqa-delivery']: it is identical on every retry of one delivery.import hashlib, hmac, time
def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
header = headers.get("x-goodqa-signature", "")
parts = [p.split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in parts if k == "t"), None)
if t is None or not t.isdigit() or abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(v, expected) for k, v in parts if k == "v1") # several during rotation
# Dedupe on headers["x-goodqa-delivery"]: it is identical on every retry of one delivery.Key scopes
A key names zero or more families. A key naming none reaches every route an API key may use except the explicit-only set; a key naming some is limited to those. GET /v1/capabilities returns the effective list as caller.apiKey.scopes. Families:
mailroadmap_suggestionsworkissue_typescommand_centerwork_dashboardorgsprojectsissuestestinguploadsaigithubdecisionsmcpagent_memoryproject_memorycontext_memoryvaultartifactsdocumentssurfacenotificationsagentschatstatsmarketingexperience_reviewsdeployinvitescmsbillboardcrmrunnerdataagent_knowledgememoryoutreachagent_launchnotes_to_selfusageusage:writemachinesescalation
Missions, step results and test plans
Missions and test plans need the testing family. Step results are written with PUT /missions/{missionId}/step-results. An API key is refused (403) on a mission whose recordingPolicy is required, and recording sessions are always started by a person; set the policy to optional for automation. Errors are catalogued at GET /v1/meta/errors.
Verification ledger
GET /issues/{issueId}/verifications lists the hash-chained attestations that a mission verified an issue; POST to the same path appends one. Each entry is chained to the one before it, so an edited or removed entry breaks verification of the rows after it. The same issue and project visibility rules as the issue apply; an issue you cannot see answers 404.
Issue links and pull requests
GET|POST /issues/{issueId}/links reads and adds links between issues; POST /issues/{issueId}/pull-requests attaches a GitHub pull request and GET /issues/{issueId}/fix-pull-requests lists the ones that fix it. Exact bodies and responses are in the endpoint reference below.
Idempotency-Key
Send Idempotency-Key on these create routes. The same key and body within 24 hours replays the first result; the same key with a different body is refused; keys are per caller. Routes:
POST /v1/orgs/:p/artifactsPOST /v1/artifacts/:p/versionsPOST /v1/artifacts/:p/linksPOST /v1/projects/:p/issuesPOST /v1/issues/:p/commentsPOST /v1/issues/:p/linksPOST /v1/issues/:p/dependenciesPOST /v1/orgs/:p/crm/contactsPOST /v1/orgs/:p/crm/companiesPOST /v1/orgs/:p/crm/records/:p/:p/notesPOST /v1/orgs/:p/dm/:pPOST /v1/projects/:p/test-plansPOST /v1/orgs/:p/agent-knowledge/:p/supersede
Changelog
GET /v1/changelog returns what shipped per release, machine-readable, with an ETag and a since filter. The MCP tool changelog returns the same.
CLI commands
The goodqa CLI reads GOODQA_API_KEY and prints JSON. The MCP tool list is above in the MCP section. Commands:
goodqa whoamigoodqa capabilitiesgoodqa issues listgoodqa issues getgoodqa issues creategoodqa issues commentgoodqa issues statusgoodqa dm sendgoodqa dm inboxgoodqa events listengoodqa knowledge searchgoodqa knowledge writegoodqa knowledge purgegoodqa followups listgoodqa followups creategoodqa followups transitiongoodqa secret-request creategoodqa secret-request statusgoodqa secret-request listgoodqa mcp config printgoodqa product searchgoodqa product describegoodqa product whats-newgoodqa product how-do-igoodqa helpgoodqa version
Reference
Endpoints
Grouped by resource. All paths are relative to https://goodqa.app/v1. Loaded live from the OpenAPI spec.