Webhooks
Receive real-time notifications about events in your PromptReports workspace. Configure webhooks to automate workflows and keep your systems in sync.
Webhooks Overview#
Webhooks allow you to receive real-time HTTP notifications when events occur in your PromptReports workspace. Instead of polling the API for changes, webhooks push updates to your application as they happen.
Real-Time Updates
Receive instant notifications when prompts are updated, reports are generated, or evaluations complete.
Secure Delivery
All webhook payloads are signed with HMAC-SHA256 for verification. Supports HTTPS only.
Automatic Retries
Failed deliveries are automatically retried with exponential backoff for up to 24 hours.
Flexible Configuration
Subscribe to specific event types and filter by resource or organization.
Configuration#
Configure webhooks through the API or the PromptReports dashboard. Each webhook endpoint can subscribe to multiple event types.
Create a Webhook Endpoint
Register the Webhook
Store the Signing Secret
Implement Signature Verification
curl -X POST "https://api.promptreports.ai/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/promptreports",
"events": [
"prompt.created",
"prompt.updated",
"report.completed",
"evaluation.completed"
],
"description": "Production webhook endpoint"
}'{
"id": "wh_abc123",
"url": "https://yourapp.com/webhooks/promptreports",
"events": [
"prompt.created",
"prompt.updated",
"report.completed",
"evaluation.completed"
],
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "active",
"createdAt": "2024-01-15T10:30:00Z"
}Save Your Webhook Secret
Payload Format#
All webhook payloads follow a consistent JSON format with event metadata and the relevant resource data.
{
"id": "evt_xyz789",
"type": "prompt.updated",
"apiVersion": "2024-01-15",
"createdAt": "2024-01-15T14:32:10Z",
"data": {
"object": "prompt",
"id": "prm_abc123",
"name": "Customer Support Response",
"content": "You are a helpful customer support agent...",
"folderId": "fld_def456",
"version": 3,
"updatedAt": "2024-01-15T14:32:10Z",
"updatedBy": "user_ghi789"
},
"metadata": {
"organizationId": "org_jkl012",
"workspaceId": "ws_mno345",
"triggeredBy": "api"
}
}| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier for this event (use for idempotency) |
| type | string | The event type (e.g., prompt.updated) |
| apiVersion | string | API version used to generate the payload |
| createdAt | string | ISO 8601 timestamp of when the event occurred |
| data | object | The resource object that triggered the event |
| data.object | string | Type of the resource (prompt, report, etc.) |
| metadata | object | Additional context about the event |
Signature Verification#
Every webhook request includes a signature header that you must verify to ensure the request is authentic. The signature is computed using HMAC-SHA256 with your webhook secret.
X-PromptReports-Signature: t=1673456789,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
X-PromptReports-Timestamp: 1673456789The signature header contains a timestamp (t) and a signature (v1). Use both to verify the request.
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
// Parse the signature header
const parts = signature.split(',');
const timestamp = parts.find(p => p.startsWith('t='))?.split('=')[1];
const expectedSig = parts.find(p => p.startsWith('v1='))?.split('=')[1];
if (!timestamp || !expectedSig) {
return false;
}
// Verify timestamp is within tolerance (5 minutes)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
console.error('Webhook timestamp outside tolerance window');
return false;
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const computedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expectedSig),
Buffer.from(computedSig)
);
}
// Express.js middleware example
app.post('/webhooks/promptreports', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-promptreports-signature'] as string;
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(payload);
// Process the event...
res.status(200).json({ received: true });
});import hmac
import hashlib
import time
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
# Parse signature header
parts = dict(p.split('=') for p in signature.split(','))
timestamp = parts.get('t')
expected_sig = parts.get('v1')
if not timestamp or not expected_sig:
return False
# Verify timestamp (5 minute tolerance)
if abs(time.time() - int(timestamp)) > 300:
return False
# Compute signature
signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
computed_sig = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(expected_sig, computed_sig)Security Warning
Event Types#
Subscribe to specific event types based on your integration needs. Events are organized by resource type.
Prompt Events#
| Event | Description | Trigger |
|---|---|---|
| prompt.created | A new prompt was created | API, Dashboard |
| prompt.updated | A prompt's content or settings changed | API, Dashboard |
| prompt.deleted | A prompt was deleted | API, Dashboard |
| prompt.version.created | A new version was published | API, Dashboard |
| prompt.executed | A prompt was executed via API | API |
Report Events#
| Event | Description | Trigger |
|---|---|---|
| report.created | Report generation started | API, Dashboard, Automation |
| report.completed | Report generation finished successfully | System |
| report.failed | Report generation failed | System |
| report.exported | Report was exported to PDF/Word | API, Dashboard |
Evaluation Events#
| Event | Description | Trigger |
|---|---|---|
| evaluation.started | Evaluation run started | API, Dashboard |
| evaluation.completed | All test cases completed | System |
| evaluation.failed | Evaluation encountered an error | System |
| evaluation.test.completed | Individual test case completed | System |
Organization Events#
| Event | Description | Trigger |
|---|---|---|
| member.invited | A new member was invited | Dashboard |
| member.joined | An invited member accepted | Dashboard |
| member.removed | A member was removed | Dashboard |
| subscription.updated | Subscription plan changed | Billing |
Retry Logic#
When webhook delivery fails, PromptReports automatically retries with exponential backoff. Failed events are retried for up to 24 hours.
| Attempt | Delay | Cumulative Time |
|---|---|---|
| 1 | Immediate | 0 seconds |
| 2 | 1 minute | 1 minute |
| 3 | 5 minutes | 6 minutes |
| 4 | 30 minutes | 36 minutes |
| 5 | 2 hours | 2.5 hours |
| 6 | 4 hours | 6.5 hours |
| 7 | 8 hours | 14.5 hours |
| 8 | 12 hours | 24+ hours (final) |
A delivery is considered successful when your endpoint returns a 2xx HTTP status code within 30 seconds. The following responses trigger retries:
- HTTP status codes 408, 429, 500, 502, 503, 504
- Connection timeout (30 seconds)
- DNS resolution failure
- TLS/SSL handshake failure
Idempotency
id field for idempotency checks. Due to retries, your endpoint may receive the same event multiple times. Store processed event IDs and skip duplicates.const processedEvents = new Set<string>(); // Use Redis/database in production
app.post('/webhooks/promptreports', async (req, res) => {
const event = req.body;
// Check for duplicate delivery
if (processedEvents.has(event.id)) {
console.log(`Skipping duplicate event: ${event.id}`);
return res.status(200).json({ received: true, duplicate: true });
}
// Process the event
try {
await processEvent(event);
processedEvents.add(event.id);
res.status(200).json({ received: true });
} catch (error) {
// Return 500 to trigger retry
res.status(500).json({ error: 'Processing failed' });
}
});Best Practices#
Respond Quickly
Return 200 immediately, then process asynchronously. Avoid timeouts by offloading heavy work.
Implement Idempotency
Track processed event IDs to handle duplicate deliveries gracefully.
Verify Signatures
Always verify webhook signatures to prevent spoofed requests.
Monitor Failures
Set up alerting for repeated delivery failures. Check the webhook dashboard for issues.
import { Queue } from 'bullmq';
const webhookQueue = new Queue('webhook-processing');
app.post('/webhooks/promptreports', async (req, res) => {
// Verify signature first
if (!verifySignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.body;
// Immediately acknowledge receipt
res.status(200).json({ received: true });
// Queue for async processing
await webhookQueue.add('process-event', {
eventId: event.id,
eventType: event.type,
payload: event,
}, {
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
});
});
// Process events asynchronously
webhookQueue.process('process-event', async (job) => {
const { eventType, payload } = job.data;
switch (eventType) {
case 'prompt.updated':
await handlePromptUpdate(payload.data);
break;
case 'report.completed':
await handleReportComplete(payload.data);
break;
// Handle other event types...
}
});Testing Webhooks#
Test your webhook implementation before going to production. PromptReports provides several tools to help you validate your setup.
curl -X POST "https://api.promptreports.ai/v1/webhooks/wh_abc123/test" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"eventType": "prompt.updated"
}'curl -X GET "https://api.promptreports.ai/v1/webhooks/wh_abc123/deliveries" \
-H "Authorization: Bearer YOUR_API_KEY"
# Response includes delivery status, response codes, and timing
{
"deliveries": [
{
"id": "del_xyz789",
"eventId": "evt_abc123",
"eventType": "prompt.updated",
"status": "success",
"httpStatus": 200,
"responseTime": 245,
"attemptNumber": 1,
"deliveredAt": "2024-01-15T14:32:15Z"
}
]
}Local Development
# Start your local server
npm run dev # Running on http://localhost:3000
# In another terminal, create a tunnel
ngrok http 3000
# Use the ngrok URL for your webhook endpoint
# Example: https://abc123.ngrok.io/webhooks/promptreports