Key featuresFeaturesPricingSecurityDocsChangelog
Log inStart for free

Getting Started

  • What is QA Note?
  • Quick Start Guide
  • Install Extension
  • Project Members

Feature Guide

  • Screenshot & Annotation
  • Session Recording
  • Maintenance Reports
  • Issue State Model
  • Multi-issue Windows
  • Storage · DOM Snapshot

Integrations

  • GitHub
  • Commit Keys
  • MCP Server
  • Slack
  • Webhook
  • Vercel
  • Existing Playwright

API Reference

  • Authentication
  • Endpoint Reference
  • Public API v1
  • Error Handling

The issue tracker you use with your AI agent. Your agent fixes; QA Note keeps the record.

Made in Seoul · © 2026 QA Note
ProductKey featuresFeaturesPricingSecurityChangelog
ResourcesDocsMCP guideChrome Extension
CompanyBrandTerms of ServicePrivacy Policy

ffgg|CEO: Songwook Han

Business Registration No.: 746-54-00870[Verify]|E-Commerce License No.: 2024-Seoul Mapo-2178

Address: 5F, 26 World Cup buk-ro 6-gil, Mapo-gu, Seoul, Republic of Korea

Email: support@qanote.app|Hosting Provider: Vercel Inc.

© 2026 QA Note. All rights reserved.

Terms of ServicePrivacy PolicyCookie Policy
  1. Home
  2. /
  3. Docs
  4. /
  5. Integrations

Webhook

Send issue events to a custom URL — for integrations beyond Slack and GitHub.

Table of Contents
  • Use Cases
  • How to Set Up
  • Event Types
  • Payload Schema
  • Signature Verification (HMAC)
  • Retry Policy
  • Troubleshooting
  • Body is empty on receipt
  • Signature verification fails
  • Too many retries

Use webhooks for integrations beyond the built-in Slack · GitHub · Vercel paths. Whenever an issue is created, updated, or commented on, a JSON payload is POSTed to the URL you set. Linear · Jira · Discord bots · Notion databases — anything that can receive HTTP can plug in.

Use Cases

SystemUse
LinearMirror QA Note issues into Linear
JiraPush only critical-priority issues into the Jira backlog
DiscordNotify the design channel on issues with the design label
NotionAppend issue metadata as rows in a Notion DB
Custom alertsInternal Slackbot · Teams · email · SMS gateway

If a direct integration already covers the path (Slack · GitHub), there is no need to duplicate it via webhook. Webhooks are the fallback for everything else.

How to Set Up

In the dashboard:

Project Settings → Integrations → Webhook → enter URL · Secret
Path: /settings/projects/[projectId]/integrations/webhook
FieldDescription
URLExternal endpoint that receives POST requests (HTTPS only)
SecretRandom string for HMAC-SHA256 signature verification (32+ chars recommended)
Subscribed eventsWhich issue.* events to receive
Active togglePause without deleting

Only HTTPS URLs are accepted. localhost and private IPs are rejected (use a tunnel like cloudflared during development).

Event Types

EventTrigger
issue.createdA new issue is created (Extension · reporter submission · dashboard)
issue.updatedTitle · body · priority change
issue.status_changedStatus moves (open → in_progress, etc.)
issue.commentedA comment is added
issue.assignedAssignee changes
issue.label_added · issue.label_removedLabels change
issue.mergedA linked PR merges (GitHub integration)
issue.deletedIssue is deleted (trash and permanent)

You can subscribe to multiple events. A project may register up to 5 webhooks today.

Payload Schema

The POST body is JSON in this shape.

json
{
  "id": "evt_01H...",
  "type": "issue.created",
  "project": { "id": "proj_xyz", "slug": "acme-app" },
  "issue": {
    "id": "iss_abc",
    "number": 42,
    "title": "Login error",
    "status": "open",
    "priority": "high",
    "url": "https://qanote.app/i/abc-42"
  },
  "actor": { "id": "usr_123", "name": "Jane Doe", "email": "jane@example.com" },
  "timestamp": "2026-05-01T09:30:12.000Z"
}
FieldTypeDescription
idstringEvent ID (stable across retries — usable as an idempotency key)
typestringEvent type (see Event Types)
projectobjectProject the issue belongs to
issueobjectCore issue fields (load full metadata via url if needed)
actorobject | nullThe user who triggered the event. null for system-triggered changes (e.g., a fix-submitted transition from commit key detection)
timestampISO 8601Event time (UTC)

Rich issue metadata (console logs, screenshots, etc.) is not included in the payload. Re-fetch via issue.url or MCP resolve_by_url when needed.

Signature Verification (HMAC)

Each webhook request includes these headers.

HeaderValue
Content-Typeapplication/json
User-AgentQANote-Webhook/1.0
X-Qanote-EventEvent type, e.g., issue.created
X-Qanote-DeliveryEvent ID (evt_01H...)
X-Qanote-Signaturesha256=<HMAC-SHA256(secret, rawBody)>

Compute HMAC over the raw body with your secret and compare timing-safe.

ts
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: string, signatureHeader: string, secret: string): boolean {
  // Header format: "sha256=<hex>"
  const [scheme, signature] = signatureHeader.split("=");
  if (scheme !== "sha256" || !signature) return false;

  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");

  // Bail early on length mismatch (timingSafeEqual throws on different lengths)
  if (expected.length !== signature.length) return false;

  return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"));
}

Compute the HMAC over the raw body string, not a re-serialized JSON object. Re-stringifying changes whitespace and key order, breaking the signature.

Retry Policy

Non-2xx responses or timeouts (10s) trigger retries.

AttemptDelay
1stImmediate
2ndAfter 30 sec
3rdAfter 5 min
4thAfter 30 min
5th (final)After 4 hours

After the 5th attempt the event is dropped and recorded under Project Dashboard → Webhook → "Recent failures" with a red marker. If the same webhook fails 50+ consecutive times within 24 hours, it is auto-disabled and you are notified.

X-Qanote-Delivery stays the same across retries. Use it as an idempotency key on the receiving side.

Troubleshooting

Body is empty on receipt

  • A JSON parser ran before your handler captured the raw body. Preserve the raw body for HMAC verification (in Next.js Route Handlers, call await request.text() once and reuse).
  • Proxy stripped the body — check body-size limits on Cloudflare · nginx · API Gateway (1MB recommended).

Signature verification fails

  • A trailing newline got added or removed in the raw body. Hand the bytes to HMAC exactly as received.
  • The secret was rotated in the dashboard but the receiver still uses the old value. Rotation has no grace window — update both sides at once.
  • Make sure your parser keeps the sha256= prefix in mind when splitting the header.

Too many retries

  • Your endpoint is returning 5xx or timing out. Check Project Dashboard → Webhook → "Recent failures" for status codes and response bodies.
  • To defer processing, return 200 immediately and handle async. 5xx triggers retries.
Previous
Slack
Next
Vercel

Table of Contents

  • Use Cases
  • How to Set Up
  • Event Types
  • Payload Schema
  • Signature Verification (HMAC)
  • Retry Policy
  • Troubleshooting
  • Body is empty on receipt
  • Signature verification fails
  • Too many retries