Skip to content

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., openplanned)
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 200 immediately 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.