Webhook Receiver

Instead of polling, receive a push notification when your job completes. This minimal Express server handles job.completed, job.failed, and job.claimed events.

Setup

cd examples/webhook-receiver
npm install

export AGENTQA_WEBHOOK_SECRET="whsec_..."  # from Settings → Webhooks
node server.js
# → Listening on port 3001

Testing locally

# Expose to the internet with ngrok
ngrok http 3001

# Copy the ngrok URL:
# e.g. https://abc123.ngrok.io/webhook
# Paste into: BetaWindow Settings → Webhooks → Add endpoint

Event payload — job.completed

{
  "type": "job.completed",
  "data": {
    "id": "job_abc123",
    "status": "complete",
    "submitted_url": "https://your-app.vercel.app",
    "tier": "standard",
    "rating": 4,
    "summary": "Signup and checkout work. Found one medium bug.",
    "bugs": [
      {
        "severity": "medium",
        "title": "Email field accepts invalid addresses",
        "steps": "1. Go /signup\n2. Enter 'user@'\n3. Submit",
        "expected": "Validation error",
        "actual": "Server 500"
      }
    ],
    "network_log_url": "/report/job_abc123/network",
    "console_log_url": "/report/job_abc123/console",
    "completed_at": "2025-04-15T14:32:10Z"
  }
}

Signature verification

Every request includes X-BetaWindow-Signature: sha256=<hmac>. The server verifies using crypto.timingSafeEqual to prevent timing attacks.

const expected = crypto
  .createHmac('sha256', WEBHOOK_SECRET)
  .update(rawBody)        // express.raw() required
  .digest('hex')

// timingSafeEqual prevents timing attacks
crypto.timingSafeEqual(
  Buffer.from(expected, 'hex'),
  Buffer.from(received, 'hex'),
)

Common integrations

case 'job.completed': {
  const job = event.data

  // Slack DM
  await slack.chat.postMessage({
    channel: '#qa-alerts',
    text: `BetaWindow: ${job.bugs.length} bug(s) in ${job.submitted_url}`,
  })

  // Jira ticket per bug
  for (const bug of job.bugs) {
    await jira.issues.createIssue({
      summary: bug.title,
      description: bug.steps,
      priority: { name: bug.severity },
    })
  }

  // GitHub commit status
  await octokit.repos.createCommitStatus({
    state: job.bugs.length === 0 ? 'success' : 'failure',
    description: `BetaWindow: ${job.rating}/5 — ${job.summary}`,
  })
}

Next steps