Webhooks¶
Subscribe to Canviq events and receive real-time HTTP notifications in your own systems.
!!! info "Coming soon" Webhooks are planned. This page documents the planned API — endpoints are not yet live.
Overview¶
When events happen in your Canviq org (new submission, status change, etc.), Canviq sends an HTTP POST to your registered endpoint. Use webhooks to:
- Sync submissions to your internal issue tracker
- Trigger Slack notifications when high-vote items change status
- Update your product analytics pipeline
Register a Webhook¶
curl -X POST https://canviq.app/api/organizations/{org_id}/webhooks \
-H "Authorization: Bearer $CANVIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/canviq",
"events": [
"submission.created",
"submission.status_changed",
"comment.created"
],
"secret": "your-webhook-secret"
}'
The secret is used to sign payloads — store it securely alongside your API key.
Response¶
{
"id": "wh_...",
"url": "https://yourapp.com/webhooks/canviq",
"events": [
"submission.created",
"submission.status_changed",
"comment.created"
],
"created_at": "2026-02-20T00:00:00Z"
}
Event Reference¶
| Event | When it fires |
|---|---|
submission.created | A new submission is created |
submission.status_changed | Submission status is updated (e.g., open → planned) |
submission.vote_added | A vote is cast on a submission |
submission.vote_removed | A vote is removed |
comment.created | A comment is posted |
member.joined | A team invite is accepted |
org.updated | Org settings are changed |
Webhook Payload Format¶
All events share a common envelope:
{
"id": "evt_01HRXYZ...",
"event": "submission.created",
"org_id": "a1b2c3d4-...",
"timestamp": "2026-02-20T12:00:00Z",
"data": { ... }
}
submission.created¶
{
"id": "evt_01HRXYZ...",
"event": "submission.created",
"org_id": "a1b2c3d4-...",
"timestamp": "2026-02-20T12:00:00Z",
"data": {
"submission": {
"id": "9d3a1f2e-...",
"title": "Add dark mode",
"description": "Would love a dark mode option.",
"type": "idea",
"status": "open",
"vote_count": 0,
"url": "https://acme.canviq.app/en/submissions/9d3a1f2e-...",
"created_at": "2026-02-20T12:00:00Z"
}
}
}
submission.status_changed¶
{
"event": "submission.status_changed",
"data": {
"submission_id": "9d3a1f2e-...",
"previous_status": "open",
"new_status": "planned",
"changed_by": "[email protected]",
"note": "Adding to Q2 roadmap"
}
}
Verifying Webhook Signatures¶
Every webhook request includes an X-Canviq-Signature header — the HMAC-SHA256 of the raw request body using your webhook secret.
Always verify the signature before processing the payload.
import { createHmac, timingSafeEqual } from 'crypto'
import type { Request, Response } from 'express'
function verifyCanviqWebhook(
rawBody: Buffer,
signature: string,
secret: string
): boolean {
const expected = createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
try {
return timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
)
} catch {
return false // length mismatch
}
}
// Express handler
app.post('/webhooks/canviq', express.raw({ type: 'application/json' }), (req: Request, res: Response) => {
const sig = req.headers['x-canviq-signature'] as string
if (!verifyCanviqWebhook(req.body as Buffer, sig, process.env.WEBHOOK_SECRET!)) {
return res.sendStatus(401)
}
const event = JSON.parse((req.body as Buffer).toString())
console.log('Received event:', event.event)
// Handle events
switch (event.event) {
case 'submission.created':
// ...
break
case 'submission.status_changed':
// ...
break
}
res.sendStatus(200)
})
import hashlib, hmac, json, os
from flask import Flask, request, abort
app = Flask(__name__)
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhooks/canviq', methods=['POST'])
def canviq_webhook():
sig = request.headers.get('X-Canviq-Signature', '')
if not verify_signature(request.data, sig, os.environ['WEBHOOK_SECRET']):
abort(401)
event = request.json
print(f"Received event: {event['event']}")
# Handle event...
return '', 200
!!! danger "Use constant-time comparison" Always use timingSafeEqual / hmac.compare_digest for signature comparison. Regular string equality (===, ==) is vulnerable to timing attacks.
Delivery Guarantees¶
- Canviq makes at least one delivery attempt per event
- On failure (non-2xx response or timeout), Canviq retries with exponential backoff: 1 min, 5 min, 30 min, 2 hours, 6 hours
- If all retries fail, the event is marked as failed and visible in Settings → Webhooks → Delivery Log
- Your endpoint must respond within 10 seconds. Return
200immediately and process async
Managing Webhooks¶
List webhooks¶
curl https://canviq.app/api/organizations/{org_id}/webhooks \
-H "Authorization: Bearer $CANVIQ_API_KEY"
Delete a webhook¶
curl -X DELETE https://canviq.app/api/organizations/{org_id}/webhooks/{webhook_id} \
-H "Authorization: Bearer $CANVIQ_API_KEY"
View delivery log¶
Available in Settings → Webhooks → Delivery Log — shows last 100 deliveries per endpoint with status codes and response bodies.