Skip to Content
Webhooks

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:

HeaderDescription
Cloove-SignatureSignature in the form t=<unix-seconds>,v1=<hmac-sha256-hex>
Cloove-Webhook-IdUnique event ID - use it to deduplicate
Cloove-Webhook-EventThe event type, e.g. order.created
Content-TypeAlways application/json

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 } }
FieldDescription
idUnique event identifier (also in the Cloove-Webhook-Id header)
typeThe event type
createdISO 8601 timestamp of when the event was generated
environmenttest or live
dataEvent-specific payload

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.

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

EventFires when
vox.call.startedA call connects and is in progress
vox.call.completedA call ends successfully
vox.call.failedA call fails or is missed
vox.recording.readyA call recording becomes available
vox.agent.updatedAn AI agent configuration changes

Messaging

EventFires when
messaging.message.receivedAn inbound WhatsApp message arrives
messaging.message.sentAn outbound message is sent
messaging.message.delivery_updatedA message’s delivery status changes
messaging.conversation.assignedA conversation is assigned to an agent

Orders

EventFires when
order.createdAn order/sale is recorded
order.updatedAn order is updated
order.cancelledAn order is cancelled
order.refundedAn order is refunded

Products & inventory

EventFires when
product.createdA product is created
product.updatedA product is updated
inventory.low_stockA product crosses its low-stock threshold
inventory.out_of_stockA product reaches zero stock

Contacts

EventFires when
contact.createdA contact is created
contact.updatedA contact is updated

Payments & wallet

EventFires when
payment.receivedA payment is received
wallet.withdrawal.requestedA withdrawal is initiated
wallet.withdrawal.completedA withdrawal settles
wallet.withdrawal.failedA withdrawal fails
wallet.depositFunds are credited to the 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 created if ordering matters.
  • Keep the secret server-side. Rotate it from the dashboard if it’s ever exposed.
Last updated on