# 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

```typescript
{
  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`, leave `projectId` null
- Project-specific: Set both `organizationId` and `projectId`

### webhookDeliveries Table

```typescript
{
  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):

```typescript
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**:

```typescript
{
  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**:

```typescript
{
  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):

```typescript
const { data } = await trpc.webhooks.list.useQuery({
  projectId: 'proj_123'
})
```

**Response**:

```typescript
{
  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):

```typescript
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**:

- `url`
- `events`
- `active` (enable/disable)

**Non-updatable**:

- `id`
- `secret` (use rotate secret instead)
- `organizationId`
- `projectId`

---

### Delete Webhook

**tRPC** (planned):

```typescript
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):

```typescript
const { mutate } = trpc.webhooks.rotateSecret.useMutation()
mutate({ webhookId: 'webhook_abc' })
```

**Response**:

```typescript
{
  secret: 'whsec_new_secret_here'  // Only shown once
}
```

**Use Case**: Secret compromised or routine rotation

---

## 📨 Webhook Payload Format

### Standard Payload Structure

```typescript
{
  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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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 verification
- `X-Izri-Event` - Event type
- `X-Izri-Delivery` - Unique delivery ID
- `X-Izri-Timestamp` - Delivery timestamp

---

### Signature Verification

**Signature Format**: `sha256=<hex_digest>`

**Algorithm**: HMAC-SHA256

**Node.js Example**:

```typescript
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**:

```python
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:

```typescript
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

```typescript
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):

```typescript
const { data } = await trpc.webhooks.deliveries.useQuery({
  webhookId: 'webhook_abc',
  limit: 50,
  offset: 0
})
```

**Response**:

```typescript
{
  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):

```typescript
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 commits
- `pull_request` - Run tests on PRs
- `release` - Trigger on releases

**Setup**:

1. Go to GitHub repo → Settings → Webhooks
2. Add webhook URL: `https://yourdomain.com/webhooks/github`
3. Set content type: `application/json`
4. Set secret: Generate random secret
5. Select events: `push`, `pull_request`
6. Save webhook

**Signature Verification** (GitHub):

```typescript
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):

```typescript
const { mutate } = trpc.webhooks.test.useMutation()
mutate({ webhookId: 'webhook_abc' })
```

**Test Payload**:

```json
{
  "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

```bash
# 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

```typescript
// 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

```typescript
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

```typescript
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](../features/webhooks.md) - Feature overview
- [REST Endpoints](./rest-endpoints.md) - Incoming webhook endpoints
- [Error Codes](./error-codes.md) - Webhook error handling
- [Testing Guidelines](../contributing/testing-guidelines.md) - Testing webhooks
