Webhooks API Reference
Complete reference for webhook configuration, delivery, and security in Izri.
๐ Overview
Webhooks allow you to receive real-time HTTP notifications when events occur in your projects.
Status: ๐ง Partially Implemented (database schema ready, endpoints planned)
Webhook Types:
- ๐ค Outgoing webhooks - Send events to external URLs
- ๐ฅ Incoming webhooks - Receive events from GitHub, etc. (planned)
๐ค Outgoing Webhooks
Send HTTP POST requests to your endpoints when events occur.
Event Types
| Event | Description | Payload |
|---|---|---|
test_run.started |
Test execution started | TestRun object |
test_run.completed |
Test execution completed | TestRun with results |
test_run.failed |
Test execution failed | TestRun with error |
test.generated |
AI generated new test | GeneratedTest object |
analysis.completed |
Repository analysis done | Analysis object |
project.created |
New project created | Project object |
project.updated |
Project settings changed | Project object |
project.deleted |
Project deleted | Project ID |
๐๏ธ Database Schema
webhooks Table
{
id: string
organizationId?: string // Org-wide or null
projectId?: string // Project-specific or null
url: string // Destination URL
events: string[] // ['test_run.completed', 'analysis.completed']
secret: string // For signature verification
active: boolean
createdAt: Date
updatedAt: Date
}
Scope Options:
- Organization-wide: Set
organizationId, leaveprojectIdnull - Project-specific: Set both
organizationIdandprojectId
webhookDeliveries Table
{
id: string
webhookId: string
event: string
payload: object // JSONB - full event payload
responseStatus?: number
responseBody?: string
error?: string
attemptCount: number
nextRetryAt?: Date
deliveredAt?: Date
createdAt: Date
}
Delivery Tracking:
- Records every webhook attempt
- Stores response status and body
- Tracks retry attempts
- Maintains delivery history
๐ง Webhook Management API
Create Webhook
tRPC (planned):
const { mutate } = trpc.webhooks.create.useMutation()
mutate({
url: 'https://api.example.com/webhooks',
events: ['test_run.completed', 'analysis.completed'],
projectId: 'proj_123', // Optional
organizationId: 'org_456' // Optional
})
Request:
{
url: string // Must be HTTPS in production
events: string[] // Event types to subscribe to
projectId?: string // Project-specific webhook
organizationId?: string // Organization webhook
secret?: string // Custom secret (auto-generated if not provided)
}
Response:
{
webhook: {
id: 'webhook_abc123',
url: 'https://api.example.com/webhooks',
events: ['test_run.completed', 'analysis.completed'],
secret: 'whsec_...', // Only shown once
active: true,
createdAt: Date
}
}
Important: Save the secret immediately - it won't be shown again!
List Webhooks
tRPC (planned):
const { data } = await trpc.webhooks.list.useQuery({
projectId: 'proj_123'
})
Response:
{
webhooks: [
{
id: 'webhook_abc',
url: 'https://api.example.com/webhooks',
events: ['test_run.completed'],
active: true,
createdAt: Date,
lastDeliveryAt?: Date,
deliveryCount: 42
}
]
}
Note: Secrets are never returned in list/get operations
Update Webhook
tRPC (planned):
const { mutate } = trpc.webhooks.update.useMutation()
mutate({
webhookId: 'webhook_abc',
url: 'https://new-url.example.com/webhooks',
events: ['test_run.completed', 'test_run.failed'],
active: true
})
Updatable Fields:
urleventsactive(enable/disable)
Non-updatable:
idsecret(use rotate secret instead)organizationIdprojectId
Delete Webhook
tRPC (planned):
const { mutate } = trpc.webhooks.delete.useMutation()
mutate({ webhookId: 'webhook_abc' })
Side Effects:
- Webhook immediately deactivated
- Pending deliveries cancelled
- Delivery history preserved for 30 days
Rotate Secret
tRPC (planned):
const { mutate } = trpc.webhooks.rotateSecret.useMutation()
mutate({ webhookId: 'webhook_abc' })
Response:
{
secret: 'whsec_new_secret_here' // Only shown once
}
Use Case: Secret compromised or routine rotation
๐จ Webhook Payload Format
Standard Payload Structure
{
id: string // Delivery ID
event: string // Event type
timestamp: string // ISO 8601
data: object // Event-specific data
project?: {
id: string
name: string
repository: string
}
organization?: {
id: string
name: string
}
}
test_run.completed
{
"id": "delivery_abc123",
"event": "test_run.completed",
"timestamp": "2024-01-01T00:00:00.000Z",
"data": {
"id": "run_xyz789",
"projectId": "proj_123",
"status": "completed",
"totalTests": 50,
"passedTests": 45,
"failedTests": 5,
"skippedTests": 0,
"duration": 12500,
"coverage": 85.5,
"createdAt": "2024-01-01T00:00:00.000Z"
},
"project": {
"id": "proj_123",
"name": "My App",
"repository": "https://github.com/user/repo"
}
}
test_run.failed
{
"id": "delivery_def456",
"event": "test_run.failed",
"timestamp": "2024-01-01T00:00:00.000Z",
"data": {
"id": "run_xyz789",
"projectId": "proj_123",
"status": "failed",
"error": "Failed to execute tests: timeout",
"log": "Test execution logs...",
"createdAt": "2024-01-01T00:00:00.000Z"
},
"project": {
"id": "proj_123",
"name": "My App",
"repository": "https://github.com/user/repo"
}
}
analysis.completed
{
"id": "delivery_ghi789",
"event": "analysis.completed",
"timestamp": "2024-01-01T00:00:00.000Z",
"data": {
"id": "analysis_abc",
"projectId": "proj_123",
"commitSha": "abc123def456",
"filesAnalyzed": 156,
"testCandidates": 23,
"languages": {
"TypeScript": 120,
"JavaScript": 36
},
"frameworks": ["react", "vite"],
"createdAt": "2024-01-01T00:00:00.000Z"
},
"project": {
"id": "proj_123",
"name": "My App",
"repository": "https://github.com/user/repo"
}
}
project.created
{
"id": "delivery_jkl012",
"event": "project.created",
"timestamp": "2024-01-01T00:00:00.000Z",
"data": {
"id": "proj_123",
"name": "My App",
"repository": "https://github.com/user/repo",
"branch": "main",
"language": "typescript",
"framework": "react",
"createdAt": "2024-01-01T00:00:00.000Z"
}
}
๐ Security & Verification
Request Headers
Every webhook request includes these headers:
POST /your-webhook-endpoint HTTP/1.1
Host: api.example.com
Content-Type: application/json
User-Agent: Izri-Webhook/1.0
X-Izri-Signature: sha256=abc123...
X-Izri-Event: test_run.completed
X-Izri-Delivery: delivery_abc123
X-Izri-Timestamp: 2024-01-01T00:00:00.000Z
Header Descriptions:
X-Izri-Signature- HMAC signature for verificationX-Izri-Event- Event typeX-Izri-Delivery- Unique delivery IDX-Izri-Timestamp- Delivery timestamp
Signature Verification
Signature Format: sha256=<hex_digest>
Algorithm: HMAC-SHA256
Node.js Example:
import crypto from 'node:crypto'
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const hmac = crypto.createHmac('sha256', secret)
const digest = `sha256=${hmac.update(payload).digest('hex')}`
// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(digest)
)
}
// Usage in Express/Hono
app.post('/webhooks', async (c) => {
const rawBody = await c.req.text()
const signature = c.req.header('X-Izri-Signature')
const secret = process.env.WEBHOOK_SECRET
if (!signature || !verifyWebhookSignature(rawBody, signature, secret)) {
return c.json({ error: 'Invalid signature' }, 401)
}
const payload = JSON.parse(rawBody)
// Process webhook...
return c.json({ received: true })
})
Python Example:
import hmac
import hashlib
def verify_webhook_signature(payload: str, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Timestamp Validation
Prevent replay attacks by checking timestamp:
function isTimestampValid(timestamp: string, toleranceSeconds = 300): boolean {
const now = Date.now()
const webhookTime = new Date(timestamp).getTime()
const diff = Math.abs(now - webhookTime) / 1000
return diff <= toleranceSeconds
}
// Usage
const timestamp = c.req.header('X-Izri-Timestamp')
if (!isTimestampValid(timestamp, 300)) {
return c.json({ error: 'Timestamp too old' }, 401)
}
Recommended tolerance: 5 minutes (300 seconds)
๐ Delivery & Retries
Delivery Guarantees
At-least-once delivery: Webhooks may be delivered multiple times
Expected Response:
- Success: HTTP 200-299
- Retry: HTTP 408, 429, 500-599
- Fail: HTTP 400-499 (except 408, 429)
Idempotency: Use X-Izri-Delivery header to detect duplicates
const deliveryId = req.headers['X-Izri-Delivery']
// Check if already processed
const exists = await redis.get(`webhook:${deliveryId}`)
if (exists) {
return res.status(200).json({ received: true })
}
// Process webhook
await processWebhook(payload)
// Mark as processed (24 hour TTL)
await redis.set(`webhook:${deliveryId}`, '1', 'EX', 86400)
Retry Strategy
Retry Schedule:
- Attempt 1: Immediate
- Attempt 2: 1 minute later
- Attempt 3: 5 minutes later
- Attempt 4: 30 minutes later
- Attempt 5: 2 hours later
- Attempt 6: 12 hours later
Max Attempts: 6
Exponential Backoff: Yes
Headers on Retry:
X-Izri-Delivery-Attempt: 3
Timeout
Request Timeout: 30 seconds
Connection Timeout: 10 seconds
Keep-Alive: Disabled (fresh connection each time)
๐ Webhook Deliveries
View Delivery History
tRPC (planned):
const { data } = await trpc.webhooks.deliveries.useQuery({
webhookId: 'webhook_abc',
limit: 50,
offset: 0
})
Response:
{
deliveries: [
{
id: 'delivery_abc',
event: 'test_run.completed',
responseStatus: 200,
attemptCount: 1,
deliveredAt: Date,
createdAt: Date
},
{
id: 'delivery_def',
event: 'test_run.failed',
responseStatus: 500,
error: 'Connection timeout',
attemptCount: 3,
nextRetryAt: Date,
createdAt: Date
}
],
pagination: {
total: 125,
limit: 50,
offset: 0
}
}
Retry Failed Delivery
tRPC (planned):
const { mutate } = trpc.webhooks.retryDelivery.useMutation()
mutate({ deliveryId: 'delivery_abc' })
Effect: Immediately retries delivery (doesn't wait for scheduled retry)
๐ฅ Incoming Webhooks
Receive webhooks from external services.
GitHub Webhooks
Endpoint: POST /webhooks/github
Events Supported:
push- Trigger analysis/tests on new commitspull_request- Run tests on PRsrelease- Trigger on releases
Setup:
- Go to GitHub repo โ Settings โ Webhooks
- Add webhook URL:
https://yourdomain.com/webhooks/github - Set content type:
application/json - Set secret: Generate random secret
- Select events:
push,pull_request - Save webhook
Signature Verification (GitHub):
import crypto from 'node:crypto'
function verifyGitHubSignature(
payload: string,
signature: string,
secret: string
): boolean {
const hmac = crypto.createHmac('sha1', secret)
const digest = `sha1=${hmac.update(payload).digest('hex')}`
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(digest)
)
}
// Usage
const signature = req.headers['x-hub-signature']
const event = req.headers['x-github-event']
if (!verifyGitHubSignature(rawBody, signature, GITHUB_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' })
}
// Handle event
if (event === 'push') {
await triggerAnalysis(payload.repository.url, payload.after)
}
๐งช Testing Webhooks
Webhook Test Endpoint
tRPC (planned):
const { mutate } = trpc.webhooks.test.useMutation()
mutate({ webhookId: 'webhook_abc' })
Test Payload:
{
"id": "delivery_test",
"event": "webhook.test",
"timestamp": "2024-01-01T00:00:00.000Z",
"data": {
"message": "This is a test webhook"
}
}
Expected Response: HTTP 200
Local Testing with ngrok
# Install ngrok
brew install ngrok
# Start ngrok tunnel
ngrok http 4000
# Use ngrok URL as webhook URL
# Example: https://abc123.ngrok.io/webhooks
Webhook URL: https://abc123.ngrok.io/webhooks
Mock Webhook Server
// test-webhook-server.ts
import { Hono } from 'hono'
const app = new Hono()
app.post('/webhooks', async (c) => {
const signature = c.req.header('X-Izri-Signature')
const event = c.req.header('X-Izri-Event')
const delivery = c.req.header('X-Izri-Delivery')
const payload = await c.req.json()
console.log('Received webhook:')
console.log('Event:', event)
console.log('Delivery:', delivery)
console.log('Payload:', JSON.stringify(payload, null, 2))
return c.json({ received: true })
})
export default app
๐ Integration Examples
Slack Integration
async function sendToSlack(webhook: WebhookPayload) {
const slackWebhookUrl = 'https://hooks.slack.com/services/...'
let message = ''
if (webhook.event === 'test_run.completed') {
const { passedTests, totalTests } = webhook.data
message = `โ
Tests completed: ${passedTests}/${totalTests} passed`
} else if (webhook.event === 'test_run.failed') {
message = `โ Tests failed: ${webhook.data.error}`
}
await fetch(slackWebhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: message,
blocks: [
{
type: 'section',
text: { type: 'mrkdwn', text: message }
},
{
type: 'context',
elements: [
{ type: 'mrkdwn', text: `Project: ${webhook.project.name}` }
]
}
]
})
})
}
Discord Integration
async function sendToDiscord(webhook: WebhookPayload) {
const discordWebhookUrl = 'https://discord.com/api/webhooks/...'
const embed = {
title: `${webhook.event} Event`,
description: formatWebhookData(webhook.data),
color: webhook.event.includes('failed') ? 0xff0000 : 0x00ff00,
timestamp: webhook.timestamp,
footer: { text: `Project: ${webhook.project.name}` }
}
await fetch(discordWebhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ embeds: [embed] })
})
}
๐ Related Documentation
- Webhooks Feature - Feature overview
- REST Endpoints - Incoming webhook endpoints
- Error Codes - Webhook error handling
- Testing Guidelines - Testing webhooks