Webhooks
Webhooks enable real-time notifications when events occur in Querri. Configure HTTP callbacks to integrate with external systems, trigger workflows, or monitor activity.
Base URL
Section titled “Base URL”https://api.querri.com/apiAuthentication
Section titled “Authentication”All Webhook API endpoints require JWT authentication. See Authentication for details.
Overview
Section titled “Overview”Webhooks send HTTP POST requests to your specified URL when events occur. Each webhook can subscribe to multiple event types and include custom headers for authentication.
Endpoints
Section titled “Endpoints”Create Webhook
Section titled “Create Webhook”Register a new webhook endpoint.
POST /api/webhooksHeaders:
Authorization: Bearer {JWT_TOKEN}Content-Type: application/jsonRequest Body:
{ "url": "https://api.example.com/querri/webhooks", "events": [ "project.completed", "project.failed", "dashboard.refreshed" ], "secret": "whsec_abc123...", "enabled": true, "headers": { "X-API-Key": "sk_live_abc123..." }, "filters": { "project_uuids": ["proj_01ABCDEF", "proj_02GHIJKL"] }, "retry_config": { "max_attempts": 3, "backoff_multiplier": 2 }}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL to send webhook payloads |
events | array | Yes | Event types to subscribe to |
secret | string | No | Secret for signature verification (auto-generated if omitted) |
enabled | boolean | No | Enable webhook (default: true) |
headers | object | No | Custom HTTP headers to include |
filters | object | No | Event filters (resource UUIDs, tags, etc.) |
retry_config | object | No | Retry configuration |
Response: 201 Created
{ "webhook_id": "hook_01ABCDEF", "url": "https://api.example.com/querri/webhooks", "events": [ "project.completed", "project.failed", "dashboard.refreshed" ], "secret": "whsec_abc123456789...", "enabled": true, "headers": { "X-API-Key": "sk_live_***" }, "filters": { "project_uuids": ["proj_01ABCDEF", "proj_02GHIJKL"] }, "retry_config": { "max_attempts": 3, "backoff_multiplier": 2, "initial_delay_seconds": 1 }, "created_by": "user_01ABCDEF", "created_at": "2025-01-15T10:00:00Z", "status": "active"}Error Responses:
// 400 Bad Request - Invalid URL{ "error": "validation_error", "message": "Webhook URL must use HTTPS", "field": "url"}
// 400 Bad Request - Invalid events{ "error": "validation_error", "message": "Unknown event type: invalid.event", "field": "events", "valid_events": ["project.completed", "project.failed", ...]}List Webhooks
Section titled “List Webhooks”Retrieve all webhooks configured for the organization.
GET /api/webhooksQuery Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | boolean | - | Filter by enabled status |
event | string | - | Filter by event type |
Example Request:
curl "https://api.querri.com/api/webhooks?enabled=true" \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."Response: 200 OK
{ "webhooks": [ { "webhook_id": "hook_01ABCDEF", "url": "https://api.example.com/querri/webhooks", "events": ["project.completed", "project.failed"], "enabled": true, "created_at": "2025-01-15T10:00:00Z", "last_triggered_at": "2025-01-15T14:30:00Z", "success_rate_24h": 98.5, "status": "active" } ], "total": 1}Get Webhook
Section titled “Get Webhook”Retrieve details for a specific webhook.
GET /api/webhooks/{webhook_id}Response: 200 OK
{ "webhook_id": "hook_01ABCDEF", "url": "https://api.example.com/querri/webhooks", "events": ["project.completed", "project.failed", "dashboard.refreshed"], "secret": "whsec_abc123...", "enabled": true, "headers": { "X-API-Key": "sk_live_***" }, "filters": { "project_uuids": ["proj_01ABCDEF", "proj_02GHIJKL"] }, "retry_config": { "max_attempts": 3, "backoff_multiplier": 2, "initial_delay_seconds": 1 }, "created_by": "user_01ABCDEF", "created_at": "2025-01-15T10:00:00Z", "updated_at": "2025-01-15T10:00:00Z", "statistics": { "total_deliveries": 142, "successful_deliveries": 140, "failed_deliveries": 2, "success_rate": 98.6, "average_response_time_ms": 245, "last_triggered_at": "2025-01-15T14:30:00Z", "last_success_at": "2025-01-15T14:30:00Z", "last_failure_at": "2025-01-14T22:15:00Z" }, "status": "active"}Update Webhook
Section titled “Update Webhook”Update webhook configuration.
PATCH /api/webhooks/{webhook_id}Request Body:
{ "url": "https://api.example.com/querri/webhooks/v2", "events": ["project.completed", "step.failed"], "enabled": true, "headers": { "X-API-Key": "sk_live_new_key_123..." }}Response: 200 OK
Returns updated webhook object.
Delete Webhook
Section titled “Delete Webhook”Permanently delete a webhook.
DELETE /api/webhooks/{webhook_id}Response: 204 No Content
Test Webhook
Section titled “Test Webhook”Send a test payload to verify webhook configuration.
POST /api/webhooks/{webhook_id}/testRequest Body:
{ "event_type": "project.completed"}Response: 200 OK
{ "test_id": "test_01ABCDEF", "webhook_id": "hook_01ABCDEF", "event_type": "project.completed", "status": "success", "response_code": 200, "response_time_ms": 234, "sent_at": "2025-01-15T10:00:00Z", "payload": { "event": "project.completed", "data": { "project_uuid": "proj_test", "name": "Test Project" } }}Error Response:
{ "test_id": "test_01ABCDEF", "webhook_id": "hook_01ABCDEF", "status": "failed", "error": "connection_timeout", "error_message": "Connection timeout after 10 seconds", "sent_at": "2025-01-15T10:00:00Z"}Event Types
Section titled “Event Types”Project Events
Section titled “Project Events”| Event | Description | Triggered When |
|---|---|---|
project.created | New project created | Project is created |
project.updated | Project modified | Project metadata or steps updated |
project.deleted | Project removed | Project is deleted |
project.executing | Execution started | Project execution begins |
project.completed | Execution succeeded | All steps complete successfully |
project.failed | Execution failed | One or more steps fail |
project.cancelled | Execution cancelled | User cancels execution |
Step Events
Section titled “Step Events”| Event | Description | Triggered When |
|---|---|---|
step.created | New step added | Step is created |
step.executing | Step started | Step execution begins |
step.completed | Step succeeded | Step completes successfully |
step.failed | Step failed | Step execution fails |
Dashboard Events
Section titled “Dashboard Events”| Event | Description | Triggered When |
|---|---|---|
dashboard.created | New dashboard created | Dashboard is created |
dashboard.updated | Dashboard modified | Widgets or settings updated |
dashboard.deleted | Dashboard removed | Dashboard is deleted |
dashboard.refreshed | Data refreshed | Dashboard refresh completes |
dashboard.refresh_failed | Refresh failed | Dashboard refresh fails |
dashboard.shared | Dashboard shared | Share link created |
Connector Events
Section titled “Connector Events”| Event | Description | Triggered When |
|---|---|---|
connector.created | New connector added | Connector is created |
connector.updated | Connector modified | Configuration updated |
connector.deleted | Connector removed | Connector is deleted |
connector.connection_failed | Connection test failed | Connector test fails |
connector.credentials_expired | OAuth token expired | OAuth refresh fails |
File Events
Section titled “File Events”| Event | Description | Triggered When |
|---|---|---|
file.uploaded | New file uploaded | File upload completes |
file.deleted | File removed | File is deleted |
file.shared | File share link created | Share link generated |
Payload Format
Section titled “Payload Format”All webhook payloads follow this structure:
{ "event": "project.completed", "event_id": "evt_01ABCDEF", "timestamp": "2025-01-15T14:30:00Z", "organization_id": "org_01ABCDEF", "data": { // Event-specific data }}Event Payload Examples
Section titled “Event Payload Examples”project.completed
Section titled “project.completed”{ "event": "project.completed", "event_id": "evt_01ABCDEF", "timestamp": "2025-01-15T14:30:00Z", "organization_id": "org_01ABCDEF", "data": { "project_uuid": "proj_01ABCDEF", "name": "Q4 Sales Analysis", "execution_id": "exec_01ABCDEF", "started_at": "2025-01-15T14:25:00Z", "completed_at": "2025-01-15T14:30:00Z", "duration_seconds": 300, "steps_completed": 5, "total_steps": 5, "triggered_by": "user_01ABCDEF", "results_url": "https://app.querri.com/projects/proj_01ABCDEF/executions/exec_01ABCDEF" }}step.failed
Section titled “step.failed”{ "event": "step.failed", "event_id": "evt_02GHIJKL", "timestamp": "2025-01-15T14:28:00Z", "organization_id": "org_01ABCDEF", "data": { "project_uuid": "proj_01ABCDEF", "project_name": "Q4 Sales Analysis", "step_id": "step_03MNOPQR", "step_name": "Calculate revenue metrics", "execution_id": "exec_01ABCDEF", "error": { "code": "sql_execution_error", "message": "Division by zero in calculation", "details": "Error at line 15: SELECT revenue / 0" }, "started_at": "2025-01-15T14:27:00Z", "failed_at": "2025-01-15T14:28:00Z" }}dashboard.refreshed
Section titled “dashboard.refreshed”{ "event": "dashboard.refreshed", "event_id": "evt_03MNOPQR", "timestamp": "2025-01-15T15:00:00Z", "organization_id": "org_01ABCDEF", "data": { "dashboard_uuid": "dash_01ABCDEF", "name": "Executive Dashboard", "refresh_id": "refresh_01ABCDEF", "started_at": "2025-01-15T14:58:00Z", "completed_at": "2025-01-15T15:00:00Z", "duration_seconds": 120, "widgets_refreshed": 8, "triggered_by": "schedule", "dashboard_url": "https://app.querri.com/dashboards/dash_01ABCDEF" }}connector.connection_failed
Section titled “connector.connection_failed”{ "event": "connector.connection_failed", "event_id": "evt_04STUVWX", "timestamp": "2025-01-15T09:00:00Z", "organization_id": "org_01ABCDEF", "data": { "connector_uuid": "conn_01ABCDEF", "connector_name": "Production Database", "connector_type": "postgresql", "error": { "type": "connection_timeout", "message": "Connection timeout after 30 seconds", "host": "db.example.com", "port": 5432 }, "tested_at": "2025-01-15T09:00:00Z", "consecutive_failures": 3 }}Webhook Security
Section titled “Webhook Security”Signature Verification
Section titled “Signature Verification”Every webhook payload includes a signature in the X-Querri-Signature header for verification.
Signature Algorithm:
HMAC-SHA256(payload_body, webhook_secret)Verification Example (Node.js):
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) { const expectedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex');
return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) );}
// Express middlewareapp.post('/webhooks', (req, res) => { const signature = req.headers['x-querri-signature']; const payload = JSON.stringify(req.body);
if (!verifyWebhook(payload, signature, webhookSecret)) { return res.status(401).send('Invalid signature'); }
// Process webhook res.status(200).send('OK');});Verification Example (Python):
import hmacimport hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool: expected_signature = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest()
return hmac.compare_digest(signature, expected_signature)
# Flask example@app.route('/webhooks', methods=['POST'])def handle_webhook(): signature = request.headers.get('X-Querri-Signature') payload = request.get_data()
if not verify_webhook(payload, signature, webhook_secret): return 'Invalid signature', 401
# Process webhook return 'OK', 200Additional Security Headers
Section titled “Additional Security Headers”Each webhook request includes:
| Header | Description |
|---|---|
X-Querri-Signature | HMAC-SHA256 signature for verification |
X-Querri-Event | Event type |
X-Querri-Event-Id | Unique event identifier (for deduplication) |
X-Querri-Timestamp | Event timestamp (Unix milliseconds) |
User-Agent | Querri-Webhooks/1.0 |
Replay Attack Prevention
Section titled “Replay Attack Prevention”Check the timestamp to prevent replay attacks:
function isRecentEvent(timestamp, maxAgeSeconds = 300) { const eventTime = parseInt(timestamp); const currentTime = Date.now(); return (currentTime - eventTime) < (maxAgeSeconds * 1000);}Retry Logic
Section titled “Retry Logic”Automatic Retries
Section titled “Automatic Retries”Webhooks are automatically retried on failure with exponential backoff:
| Attempt | Delay | Total Time |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 1s | 1s |
| 3 | 2s | 3s |
| 4 | 4s | 7s |
| 5 | 8s | 15s |
Retry Conditions:
- HTTP status codes: 408, 429, 500, 502, 503, 504
- Connection timeout (10 seconds)
- DNS resolution failure
No Retry Conditions:
- HTTP status codes: 200-299 (success)
- HTTP status codes: 400-499 (except 408, 429)
- Invalid URL
- SSL certificate errors
Custom Retry Configuration
Section titled “Custom Retry Configuration”{ "retry_config": { "max_attempts": 5, "initial_delay_seconds": 1, "backoff_multiplier": 2, "max_delay_seconds": 60 }}Webhook Delivery Status
Section titled “Webhook Delivery Status”curl "https://api.querri.com/api/webhooks/hook_01ABCDEF/deliveries" \ -H "Authorization: Bearer ${JWT_TOKEN}"Response:
{ "webhook_id": "hook_01ABCDEF", "deliveries": [ { "delivery_id": "del_01ABCDEF", "event_id": "evt_01ABCDEF", "event": "project.completed", "status": "success", "attempts": 1, "response_code": 200, "response_time_ms": 245, "created_at": "2025-01-15T14:30:00Z", "delivered_at": "2025-01-15T14:30:00Z" }, { "delivery_id": "del_02GHIJKL", "event_id": "evt_02GHIJKL", "event": "step.failed", "status": "failed", "attempts": 3, "response_code": 500, "error": "Internal Server Error", "created_at": "2025-01-15T14:28:00Z", "last_attempt_at": "2025-01-15T14:28:15Z", "next_retry_at": null } ], "pagination": { "page": 1, "limit": 50, "total": 142 }}Retry Specific Delivery
Section titled “Retry Specific Delivery”curl -X POST "https://api.querri.com/api/webhooks/hook_01ABCDEF/deliveries/del_02GHIJKL/retry" \ -H "Authorization: Bearer ${JWT_TOKEN}"Event Filters
Section titled “Event Filters”Reduce noise by filtering events:
Filter by Resource
Section titled “Filter by Resource”{ "events": ["project.completed", "project.failed"], "filters": { "project_uuids": ["proj_01ABCDEF", "proj_02GHIJKL"] }}Filter by Tags
Section titled “Filter by Tags”{ "events": ["step.completed"], "filters": { "tags": ["production", "critical"] }}Filter by Status
Section titled “Filter by Status”{ "events": ["step.failed"], "filters": { "error_codes": ["sql_execution_error", "connection_timeout"] }}Webhook Management
Section titled “Webhook Management”Disable Webhook
Section titled “Disable Webhook”Temporarily disable without deleting:
curl -X POST "https://api.querri.com/api/webhooks/hook_01ABCDEF/disable" \ -H "Authorization: Bearer ${JWT_TOKEN}"Response:
{ "webhook_id": "hook_01ABCDEF", "enabled": false, "disabled_at": "2025-01-15T10:00:00Z", "status": "disabled"}Re-enable Webhook
Section titled “Re-enable Webhook”curl -X POST "https://api.querri.com/api/webhooks/hook_01ABCDEF/enable" \ -H "Authorization: Bearer ${JWT_TOKEN}"Webhook Status Monitoring
Section titled “Webhook Status Monitoring”Querri automatically monitors webhook health:
- Active: Receiving and delivering events successfully
- Degraded: Success rate below 95% in last 24 hours
- Failing: Last 5 consecutive deliveries failed
- Disabled: Manually disabled or automatically paused
Automatically disabled after 100 consecutive failures.
Testing Webhooks
Section titled “Testing Webhooks”Local Testing with ngrok
Section titled “Local Testing with ngrok”# Start ngrok tunnelngrok http 3000
# Use ngrok URL in webhook configurationcurl -X POST "https://api.querri.com/api/webhooks" \ -H "Authorization: Bearer ${JWT_TOKEN}" \ -d '{ "url": "https://abc123.ngrok.io/webhooks", "events": ["project.completed"] }'Mock Webhook Server (Node.js)
Section titled “Mock Webhook Server (Node.js)”const express = require('express');const app = express();
app.use(express.json());
app.post('/webhooks', (req, res) => { console.log('Received webhook:', req.body); console.log('Signature:', req.headers['x-querri-signature']);
// Process event const { event, data } = req.body;
switch(event) { case 'project.completed': console.log('Project completed:', data.project_uuid); break; case 'step.failed': console.log('Step failed:', data.step_id, data.error); break; }
res.status(200).send('OK');});
app.listen(3000, () => { console.log('Webhook server listening on port 3000');});Best Practices
Section titled “Best Practices”- Always Verify Signatures - Prevent unauthorized webhook calls
- Respond Quickly - Return 200 OK within 10 seconds, process async
- Handle Duplicates - Use
event_idfor idempotent processing - Monitor Delivery Status - Check webhook statistics regularly
- Use Filters - Reduce unnecessary webhook calls
- Implement Retry Logic - Handle transient failures gracefully
- Log All Events - Keep audit trail of webhook deliveries
- Secure Your Endpoint - Use HTTPS and validate signatures
- Test Thoroughly - Use test endpoint before going live
- Set Up Alerts - Monitor for webhook failures
Rate Limiting
Section titled “Rate Limiting”Webhook deliveries have rate limits:
| Plan | Max Webhooks | Deliveries/Hour |
|---|---|---|
| Free | 2 | 100 |
| Pro | 10 | 1,000 |
| Enterprise | Unlimited | Unlimited |
HTTP Status Codes
Section titled “HTTP Status Codes”| Code | Description |
|---|---|
| 200 | Success (GET, PATCH, test) |
| 201 | Created (POST webhook) |
| 204 | No Content (DELETE) |
| 400 | Bad Request (validation error) |
| 401 | Unauthorized |
| 403 | Forbidden (rate limit exceeded) |
| 404 | Not Found |
| 429 | Too Many Requests |
| 500 | Internal Server Error |