Webhooks
Webhooks let Cloove notify your application the moment something happens - a call
completes, an order is created, a withdrawal settles - instead of you polling for changes.
When an event fires, Cloove sends a signed HTTPS POST to the endpoint URLs you’ve
registered.
Set up an endpoint
Register an endpoint
In the Dashboard under Developer → Webhooks, add an HTTPS
URL and select the events to subscribe to - or do it via the
Developer Portal API. Endpoints are scoped to a
single environment (test or live).
Grab your signing secret
Each environment has one signing secret (prefixed whsec_). Copy it from the dashboard
or the Portal API and store it as a server-side secret - you’ll use it to verify every
delivery.
Verify and respond
On each delivery, verify the signature, then respond 2xx
quickly. Do real work asynchronously.
Request format
Every delivery is a POST with a JSON body and these headers:
The body is a consistent envelope:
{
"id": "evt_9b2e1c7a-...",
"type": "vox.call.completed",
"created": "2026-01-15T10:02:02.000Z",
"environment": "live",
"data": {
"callId": "c1a2b3c4-...",
"status": "completed",
"direction": "outbound",
"durationSeconds": 122,
"recordingUrl": null
}
}Verifying signatures
Always verify that a delivery genuinely came from Cloove before trusting it.
The Cloove-Signature header contains a timestamp t and a signature v1. To verify,
compute HMAC-SHA256(secret, "{t}.{rawBody}") and compare it to v1 in constant time.
Verify against the raw, unparsed request body. Re-serializing the JSON (e.g.
JSON.stringify(req.body)) changes the bytes and the signature will not match. Capture
the raw body in your framework before any JSON parsing.
Node.js
const crypto = require('crypto')
function verifyClooveWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((kv) => kv.split('='))
)
const timestamp = Number(parts.t)
const provided = parts.v1
if (!timestamp || !provided) return false
// Reject stale deliveries (replay protection).
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(provided)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}Event catalog
Subscribe to any combination of these events when you create an endpoint.
Vox
Messaging
Orders
Products & inventory
Contacts
Payments & wallet
Delivery & retries
A delivery succeeds when your endpoint responds with a 2xx status within 10 seconds.
Any other response (or a timeout) is a failure and is retried.
- Retries: up to 5 attempts with exponential backoff, starting at ~5 seconds.
- Auto-disable: an endpoint that fails 15 consecutive deliveries is automatically disabled. Re-enable it from the dashboard once your receiver is healthy.
- Manual resend: you can resend any delivery from the dashboard or the Portal API - it re-sends the exact original bytes.
Best practices
- Verify every request with the signing secret, and reject unverified deliveries.
- Respond fast (
2xx), then process asynchronously - queue the event and return immediately so you never hit the 10-second timeout. - Deduplicate on
id. Retries and resends can deliver the same event more than once; treat handling as idempotent. - Don’t trust order. Events can arrive out of sequence; use
createdif ordering matters. - Keep the secret server-side. Rotate it from the dashboard if it’s ever exposed.