Outgoing webhooks

Outgoing webhooks let you connect Quelvo to any other service. When something happens in your portal — a client logs in, submits a form, or leaves a comment — Quelvo sends a signed HTTP POST to your endpoint. Use this to post messages to Slack, create tickets in your CRM, trigger automations in Make or Zapier, or feed data into your own backend. Webhooks are a Pro and Agency feature.

Events you can subscribe to

EventWhen it fires
client.loginA client successfully logs in
form.submittedA client submits a form
comment.createdA client (or owner) posts a new comment
test.pingYou click the Test button in the dashboard

You can subscribe a single webhook to multiple events, or create separate webhooks per event.

Create a webhook

  1. Open the Webhooks page

    From the portal's sidebar, click Webhooks, then New webhook.

  2. Enter a destination URL

    The URL must be publicly reachable and respond with HTTP 2xx within 10 seconds. HTTPS is strongly recommended.

  3. Pick events to subscribe to

    Check the events you want this webhook to receive.

  4. Give it a label (optional)

    A human-readable label for your own reference (e.g. "Slack #client-activity").

  5. Save — copy the signing secret

    After saving, Quelvo shows your signing secret once. Copy it now — it won't be shown again. You'll use it to verify payloads on your server.

The payload format

Every webhook delivery shares the same envelope structure:

{
  "id": "del_abc123",
  "event": "form.submitted",
  "occurredAt": "2026-05-15T09:00:00Z",
  "portal": {
    "id": "prt_xyz",
    "slug": "acme-projects",
    "name": "Acme Projects"
  },
  "data": { ... }
}

The data field varies by event:

client.login

{
  "client": { "id": "...", "email": "client@acme.com", "name": "Acme Contact" },
  "loginCount": 5,
  "lastLoginAt": "2026-05-14T08:30:00Z"
}

form.submitted

{
  "form": { "id": "...", "name": "New project brief" },
  "submission": { "id": "...", "notionPageId": "..." },
  "client": { "id": "...", "email": "client@acme.com", "name": "Acme Contact" }
}

comment.created

{
  "comment": { "id": "...", "body": "Can we adjust the deadline?", "createdAt": "..." },
  "author": { "id": "...", "email": "client@acme.com", "name": "Acme Contact" },
  "target": { "type": "page", "pageId": "...", "title": "Q3 Campaign Brief" }
}

Verifying signatures

Every request includes an X-Quelvo-Signature header:

X-Quelvo-Signature: t=1715772000,v1=abc123def456...

To verify the signature on your server:

  1. Extract t (Unix timestamp) and v1 (HMAC hex digest) from the header.
  2. Reject requests where t is more than 300 seconds old (replay protection).
  3. Concatenate t + "." + rawBody (the raw request body as a string, not parsed JSON).
  4. Compute HMAC-SHA256(signingSecret, concatenated) and hex-encode it.
  5. Compare your result to v1 using a constant-time comparison. If they match, the payload is genuine.
// Example (Node.js / TypeScript)
import { createHmac, timingSafeEqual } from 'crypto'

function verifyWebhookSignature(
  rawBody: string,
  header: string,
  secret: string
): boolean {
  const [tPart, v1Part] = header.split(',')
  const t = tPart.replace('t=', '')
  const v1 = v1Part.replace('v1=', '')

  // Reject stale payloads
  if (Date.now() / 1000 - Number(t) > 300) return false

  const expected = createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex')

  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}

Always verify signatures

Without signature verification, anyone who discovers your webhook URL could send fake payloads. Always verify before trusting the payload.

Testing a webhook

Click Test on any webhook card. Quelvo sends a test.ping payload immediately and shows the HTTP response status in a toast. If the request fails, the delivery detail shows the full response body to help debug.

Retry behavior

Quelvo makes one attempt plus one inline retry (2-second gap) if the first attempt returns a 5xx or network error. Failed deliveries are shown in the Recent deliveries panel on the webhook card. You can Replay any failed delivery manually.

After 5 consecutive failures across all deliveries, the webhook is automatically deactivated and a banner appears on the card. To re-enable, fix the receiving server, then toggle the webhook back on — this also resets the failure count.

Delivery history

Each webhook card has an expandable Recent deliveries panel showing the last 20 deliveries. Each entry shows:

  • Event name
  • Delivery timestamp
  • HTTP status code returned by your server
  • Error message (if the request failed)
  • Attempt count

Click Replay on any failed delivery to retry it immediately.

Rotating the signing secret

Click Rotate secret on a webhook card. The old secret is immediately invalidated — update your server with the new secret before rotating.

Plan limits

PlanWebhooks per portal
StarterNot available
ProUp to 10
AgencyUp to 10