Skip to content

Webhooks

Webhooks enable real-time notifications when events occur in Querri. Configure HTTP callbacks to integrate with external systems, trigger workflows, or monitor activity.

https://api.querri.com/api

All Webhook API endpoints require JWT authentication. See Authentication for details.

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.


Register a new webhook endpoint.

POST /api/webhooks

Headers:

Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json

Request 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:

FieldTypeRequiredDescription
urlstringYesHTTPS URL to send webhook payloads
eventsarrayYesEvent types to subscribe to
secretstringNoSecret for signature verification (auto-generated if omitted)
enabledbooleanNoEnable webhook (default: true)
headersobjectNoCustom HTTP headers to include
filtersobjectNoEvent filters (resource UUIDs, tags, etc.)
retry_configobjectNoRetry 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", ...]
}

Retrieve all webhooks configured for the organization.

GET /api/webhooks

Query Parameters:

ParameterTypeDefaultDescription
enabledboolean-Filter by enabled status
eventstring-Filter by event type

Example Request:

Terminal window
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
}

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 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.


Permanently delete a webhook.

DELETE /api/webhooks/{webhook_id}

Response: 204 No Content


Send a test payload to verify webhook configuration.

POST /api/webhooks/{webhook_id}/test

Request 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"
}

EventDescriptionTriggered When
project.createdNew project createdProject is created
project.updatedProject modifiedProject metadata or steps updated
project.deletedProject removedProject is deleted
project.executingExecution startedProject execution begins
project.completedExecution succeededAll steps complete successfully
project.failedExecution failedOne or more steps fail
project.cancelledExecution cancelledUser cancels execution
EventDescriptionTriggered When
step.createdNew step addedStep is created
step.executingStep startedStep execution begins
step.completedStep succeededStep completes successfully
step.failedStep failedStep execution fails
EventDescriptionTriggered When
dashboard.createdNew dashboard createdDashboard is created
dashboard.updatedDashboard modifiedWidgets or settings updated
dashboard.deletedDashboard removedDashboard is deleted
dashboard.refreshedData refreshedDashboard refresh completes
dashboard.refresh_failedRefresh failedDashboard refresh fails
dashboard.sharedDashboard sharedShare link created
EventDescriptionTriggered When
connector.createdNew connector addedConnector is created
connector.updatedConnector modifiedConfiguration updated
connector.deletedConnector removedConnector is deleted
connector.connection_failedConnection test failedConnector test fails
connector.credentials_expiredOAuth token expiredOAuth refresh fails
EventDescriptionTriggered When
file.uploadedNew file uploadedFile upload completes
file.deletedFile removedFile is deleted
file.sharedFile share link createdShare link generated

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": "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"
}
}
{
"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"
}
}
{
"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"
}
}
{
"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
}
}

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 middleware
app.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 hmac
import 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', 200

Each webhook request includes:

HeaderDescription
X-Querri-SignatureHMAC-SHA256 signature for verification
X-Querri-EventEvent type
X-Querri-Event-IdUnique event identifier (for deduplication)
X-Querri-TimestampEvent timestamp (Unix milliseconds)
User-AgentQuerri-Webhooks/1.0

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);
}

Webhooks are automatically retried on failure with exponential backoff:

AttemptDelayTotal Time
1Immediate0s
21s1s
32s3s
44s7s
58s15s

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
{
"retry_config": {
"max_attempts": 5,
"initial_delay_seconds": 1,
"backoff_multiplier": 2,
"max_delay_seconds": 60
}
}
Terminal window
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
}
}
Terminal window
curl -X POST "https://api.querri.com/api/webhooks/hook_01ABCDEF/deliveries/del_02GHIJKL/retry" \
-H "Authorization: Bearer ${JWT_TOKEN}"

Reduce noise by filtering events:

{
"events": ["project.completed", "project.failed"],
"filters": {
"project_uuids": ["proj_01ABCDEF", "proj_02GHIJKL"]
}
}
{
"events": ["step.completed"],
"filters": {
"tags": ["production", "critical"]
}
}
{
"events": ["step.failed"],
"filters": {
"error_codes": ["sql_execution_error", "connection_timeout"]
}
}

Temporarily disable without deleting:

Terminal window
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"
}
Terminal window
curl -X POST "https://api.querri.com/api/webhooks/hook_01ABCDEF/enable" \
-H "Authorization: Bearer ${JWT_TOKEN}"

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.


Terminal window
# Start ngrok tunnel
ngrok http 3000
# Use ngrok URL in webhook configuration
curl -X POST "https://api.querri.com/api/webhooks" \
-H "Authorization: Bearer ${JWT_TOKEN}" \
-d '{
"url": "https://abc123.ngrok.io/webhooks",
"events": ["project.completed"]
}'
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');
});

  1. Always Verify Signatures - Prevent unauthorized webhook calls
  2. Respond Quickly - Return 200 OK within 10 seconds, process async
  3. Handle Duplicates - Use event_id for idempotent processing
  4. Monitor Delivery Status - Check webhook statistics regularly
  5. Use Filters - Reduce unnecessary webhook calls
  6. Implement Retry Logic - Handle transient failures gracefully
  7. Log All Events - Keep audit trail of webhook deliveries
  8. Secure Your Endpoint - Use HTTPS and validate signatures
  9. Test Thoroughly - Use test endpoint before going live
  10. Set Up Alerts - Monitor for webhook failures

Webhook deliveries have rate limits:

PlanMax WebhooksDeliveries/Hour
Free2100
Pro101,000
EnterpriseUnlimitedUnlimited

CodeDescription
200Success (GET, PATCH, test)
201Created (POST webhook)
204No Content (DELETE)
400Bad Request (validation error)
401Unauthorized
403Forbidden (rate limit exceeded)
404Not Found
429Too Many Requests
500Internal Server Error