openevidence-webhooks-events
Configure OpenEvidence webhooks for async DeepConsult completion and events. Use when implementing webhook handlers, configuring async notifications, or setting up event-driven clinical AI workflows. Trigger with phrases like "openevidence webhook", "openevidence events", "deepconsult callback", "openevidence notifications". allowed-tools: Read, Write, Edit version: 1.0.0 license: MIT author: Jeremy Longshore <jeremy@intentsolutions.io>
Allowed Tools
No tools specified
Provided by Plugin
openevidence-pack
Claude Code skill pack for OpenEvidence medical AI (24 skills)
Installation
This skill is included in the openevidence-pack plugin:
/plugin install openevidence-pack@claude-code-plugins-plus
Click to copy
Instructions
# OpenEvidence Webhooks & Events
## Overview
Configure webhooks for asynchronous OpenEvidence operations like DeepConsult completion notifications.
## Prerequisites
- OpenEvidence Enterprise account with webhook access
- HTTPS endpoint for receiving webhooks
- Webhook secret for signature verification
- Understanding of async processing patterns
## Webhook Events
| Event Type | Description | Payload |
|------------|-------------|---------|
| `deepconsult.started` | DeepConsult processing began | `consultId`, `estimatedTime` |
| `deepconsult.progress` | Processing progress update | `consultId`, `progress` (0-100) |
| `deepconsult.completed` | DeepConsult finished successfully | `consultId`, `report` |
| `deepconsult.failed` | DeepConsult processing failed | `consultId`, `error` |
| `rate_limit.warning` | Approaching rate limit | `remaining`, `limit`, `resetAt` |
| `api_key.expiring` | API key expiration warning | `keyId`, `expiresAt` |
## Instructions
### Step 1: Configure Webhook Endpoint
```typescript
// src/webhooks/openevidence-webhook.ts
import { Router, Request, Response } from 'express';
import crypto from 'crypto';
const router = Router();
// Webhook secret from OpenEvidence dashboard
const WEBHOOK_SECRET = process.env.OPENEVIDENCE_WEBHOOK_SECRET!;
interface WebhookPayload {
id: string;
event: string;
timestamp: string;
data: any;
}
// Signature verification middleware
function verifySignature(req: Request, res: Response, next: Function) {
const signature = req.headers['x-openevidence-signature'] as string;
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
// Parse signature: t=timestamp,v1=signature
const parts = signature.split(',').reduce((acc, part) => {
const [key, value] = part.split('=');
acc[key] = value;
return acc;
}, {} as Record);
const timestamp = parseInt(parts['t']);
const providedSig = parts['v1'];
// Reject if timestamp too old (5 minute tolerance)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) {
return res.status(401).json({ error: 'Timestamp too old' });
}
// Compute expected signature
const payload = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
const signedPayload = `${timestamp}.${payload}`;
const expectedSig = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
// Timing-safe comparison
try {
const valid = crypto.timingSafeEqual(
Buffer.from(providedSig),
Buffer.from(expectedSig)
);
if (!valid) throw new Error('Invalid signature');
} catch {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
}
router.post(
'/webhooks/openevidence',
verifySignature,
async (req: Request, res: Response) => {
const payload: WebhookPayload = req.body;
console.log(`[Webhook] Received event: ${payload.event}`);
try {
await handleWebhookEvent(payload);
res.status(200).json({ received: true });
} catch (error: any) {
console.error(`[Webhook] Processing failed:`, error);
// Return 200 to prevent retries for processing errors
// OpenEvidence will retry on 4xx/5xx
res.status(200).json({ received: true, error: error.message });
}
}
);
export default router;
```
### Step 2: Event Handlers
```typescript
// src/webhooks/event-handlers.ts
import { WebhookPayload } from './types';
import { notificationService } from '../services/notifications';
import { db } from '../db';
export async function handleWebhookEvent(payload: WebhookPayload): Promise {
switch (payload.event) {
case 'deepconsult.started':
await handleDeepConsultStarted(payload);
break;
case 'deepconsult.progress':
await handleDeepConsultProgress(payload);
break;
case 'deepconsult.completed':
await handleDeepConsultCompleted(payload);
break;
case 'deepconsult.failed':
await handleDeepConsultFailed(payload);
break;
case 'rate_limit.warning':
await handleRateLimitWarning(payload);
break;
case 'api_key.expiring':
await handleApiKeyExpiring(payload);
break;
default:
console.log(`[Webhook] Unknown event type: ${payload.event}`);
}
}
async function handleDeepConsultStarted(payload: WebhookPayload): Promise {
const { consultId, estimatedTime } = payload.data;
await db.deepConsults.update({
where: { consultId },
data: {
status: 'processing',
estimatedCompletionAt: new Date(Date.now() + estimatedTime * 1000),
},
});
}
async function handleDeepConsultProgress(payload: WebhookPayload): Promise {
const { consultId, progress, currentPhase } = payload.data;
await db.deepConsults.update({
where: { consultId },
data: {
progress,
currentPhase,
},
});
// Notify user if subscribed to progress updates
const consult = await db.deepConsults.findUnique({ where: { consultId } });
if (consult?.notifyOnProgress) {
await notificationService.sendProgress(consult.userId, consultId, progress);
}
}
async function handleDeepConsultCompleted(payload: WebhookPayload): Promise {
const { consultId, report } = payload.data;
// Store the report
await db.deepConsults.update({
where: { consultId },
data: {
status: 'completed',
report,
completedAt: new Date(),
},
});
// Get user info
const consult = await db.deepConsults.findUnique({
where: { consultId },
include: { user: true },
});
if (consult) {
// Send notification
await notificationService.send({
userId: consult.userId,
type: 'deepconsult_ready',
title: 'DeepConsult Research Complete',
body: `Your research synthesis for "${consult.question.substring(0, 50)}..." is ready.`,
data: { consultId },
});
// Send email if enabled
if (consult.user.emailNotifications) {
await notificationService.sendEmail({
to: consult.user.email,
subject: 'Your OpenEvidence DeepConsult is Ready',
template: 'deepconsult-complete',
data: {
userName: consult.user.name,
consultId,
summary: report.executiveSummary.substring(0, 200),
},
});
}
}
}
async function handleDeepConsultFailed(payload: WebhookPayload): Promise {
const { consultId, error, retryable } = payload.data;
await db.deepConsults.update({
where: { consultId },
data: {
status: 'failed',
error,
retryable,
},
});
// Alert operations team
await notificationService.alertOps({
severity: 'warning',
service: 'openevidence',
message: `DeepConsult ${consultId} failed: ${error}`,
retryable,
});
}
async function handleRateLimitWarning(payload: WebhookPayload): Promise {
const { remaining, limit, resetAt } = payload.data;
console.warn(`[RateLimit] Warning: ${remaining}/${limit} remaining, resets at ${resetAt}`);
// Alert if critical
if (remaining < limit * 0.1) {
await notificationService.alertOps({
severity: 'critical',
service: 'openevidence',
message: `Rate limit critical: ${remaining}/${limit} remaining`,
});
}
}
async function handleApiKeyExpiring(payload: WebhookPayload): Promise {
const { keyId, expiresAt } = payload.data;
await notificationService.alertOps({
severity: 'warning',
service: 'openevidence',
message: `API key ${keyId} expires at ${expiresAt}`,
action: 'Rotate key before expiration',
});
}
```
### Step 3: Webhook Registration
```typescript
// src/services/webhook-registration.ts
import { OpenEvidenceClient } from '@openevidence/sdk';
export async function registerWebhooks(): Promise {
const client = new OpenEvidenceClient({
apiKey: process.env.OPENEVIDENCE_API_KEY!,
orgId: process.env.OPENEVIDENCE_ORG_ID!,
});
const webhookUrl = process.env.WEBHOOK_BASE_URL + '/webhooks/openevidence';
await client.webhooks.register({
url: webhookUrl,
events: [
'deepconsult.started',
'deepconsult.progress',
'deepconsult.completed',
'deepconsult.failed',
'rate_limit.warning',
'api_key.expiring',
],
secret: process.env.OPENEVIDENCE_WEBHOOK_SECRET!,
});
console.log(`[Webhooks] Registered: ${webhookUrl}`);
}
// Call during app startup
registerWebhooks().catch(console.error);
```
### Step 4: Idempotency Handling
```typescript
// src/webhooks/idempotency.ts
import { db } from '../db';
const IDEMPOTENCY_TTL = 24 * 60 * 60 * 1000; // 24 hours
export async function isProcessed(webhookId: string): Promise {
const existing = await db.processedWebhooks.findUnique({
where: { id: webhookId },
});
return !!existing;
}
export async function markProcessed(webhookId: string): Promise {
await db.processedWebhooks.create({
data: {
id: webhookId,
processedAt: new Date(),
expiresAt: new Date(Date.now() + IDEMPOTENCY_TTL),
},
});
}
// Cleanup job
export async function cleanupExpiredWebhooks(): Promise {
await db.processedWebhooks.deleteMany({
where: {
expiresAt: { lt: new Date() },
},
});
}
// Usage in webhook handler
async function handleWebhookWithIdempotency(payload: WebhookPayload): Promise {
if (await isProcessed(payload.id)) {
console.log(`[Webhook] Already processed: ${payload.id}`);
return;
}
await handleWebhookEvent(payload);
await markProcessed(payload.id);
}
```
### Step 5: Webhook Testing
```typescript
// tests/webhooks/openevidence-webhook.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import request from 'supertest';
import crypto from 'crypto';
import app from '../../src/app';
const WEBHOOK_SECRET = 'test-secret';
process.env.OPENEVIDENCE_WEBHOOK_SECRET = WEBHOOK_SECRET;
function generateSignature(payload: object, timestamp: number): string {
const payloadString = JSON.stringify(payload);
const signedPayload = `${timestamp}.${payloadString}`;
const signature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
return `t=${timestamp},v1=${signature}`;
}
describe('OpenEvidence Webhooks', () => {
it('should accept valid webhook', async () => {
const timestamp = Math.floor(Date.now() / 1000);
const payload = {
id: 'webhook-123',
event: 'deepconsult.completed',
timestamp: new Date().toISOString(),
data: {
consultId: 'consult-456',
report: { executiveSummary: 'Test report' },
},
};
const response = await request(app)
.post('/webhooks/openevidence')
.set('x-openevidence-signature', generateSignature(payload, timestamp))
.send(payload);
expect(response.status).toBe(200);
expect(response.body.received).toBe(true);
});
it('should reject webhook with invalid signature', async () => {
const payload = { event: 'test' };
const response = await request(app)
.post('/webhooks/openevidence')
.set('x-openevidence-signature', 't=123,v1=invalid')
.send(payload);
expect(response.status).toBe(401);
});
it('should reject webhook with old timestamp', async () => {
const oldTimestamp = Math.floor(Date.now() / 1000) - 600; // 10 minutes ago
const payload = { event: 'test' };
const response = await request(app)
.post('/webhooks/openevidence')
.set('x-openevidence-signature', generateSignature(payload, oldTimestamp))
.send(payload);
expect(response.status).toBe(401);
});
});
```
## Output
- Secure webhook endpoint with signature verification
- Event handlers for all OpenEvidence events
- Idempotency protection
- Notification integration
- Comprehensive test coverage
## Webhook Security Checklist
- [ ] HTTPS endpoint only
- [ ] Signature verification enabled
- [ ] Timestamp validation (replay protection)
- [ ] Idempotency handling
- [ ] Secret rotation procedure documented
## Error Handling
| Webhook Issue | Detection | Resolution |
|---------------|-----------|------------|
| Invalid signature | 401 response | Check secret configuration |
| Missing events | No handler called | Verify webhook registration |
| Duplicate processing | Multiple notifications | Enable idempotency |
| Timeout | Webhook fails | Process async, return 200 quickly |
## Resources
- [OpenEvidence API Docs](https://docs.openevidence.com/)
- [Webhook Security Best Practices](https://webhooks.fyi/)
## Next Steps
For performance optimization, see `openevidence-performance-tuning`.