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
| Event | When it fires |
|---|---|
client.login | A client successfully logs in |
form.submitted | A client submits a form |
comment.created | A client (or owner) posts a new comment |
test.ping | You 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
Open the Webhooks page
From the portal's sidebar, click Webhooks, then New webhook.
Enter a destination URL
The URL must be publicly reachable and respond with HTTP 2xx within 10 seconds. HTTPS is strongly recommended.
Pick events to subscribe to
Check the events you want this webhook to receive.
Give it a label (optional)
A human-readable label for your own reference (e.g. "Slack #client-activity").
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:
- Extract
t(Unix timestamp) andv1(HMAC hex digest) from the header. - Reject requests where
tis more than 300 seconds old (replay protection). - Concatenate
t + "." + rawBody(the raw request body as a string, not parsed JSON). - Compute
HMAC-SHA256(signingSecret, concatenated)and hex-encode it. - Compare your result to
v1using 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
| Plan | Webhooks per portal |
|---|---|
| Starter | Not available |
| Pro | Up to 10 |
| Agency | Up to 10 |