Webhooks
Cloove webhooks send events to your server when calls, messages, orders, products, payments, wallet activity, contacts, or hotel operations change.
This page covers outbound developer webhooks. Provider callbacks sent into Cloove by payment, telephony, or messaging providers are separate internal integrations.
Delivery model
- A Cloove operation produces an event.
- Cloove creates one delivery for each active endpoint subscribed to that event.
- A background worker signs the persisted JSON payload and sends an HTTPS
POST. - A
2xxresponse marks that endpoint delivery successful. - Timeouts, network errors, redirects, and non-
2xxresponses are retried.
Webhook delivery is at least once. The same event can reach your endpoint more than once, and separate events can arrive out of order.
Configure an endpoint
Webhook configuration uses the authenticated Developer Portal API, not a public API key.
Requests require a dashboard JWT, the MANAGE_DEVELOPER_KEYS permission, and an active
business context.
Base path: /api/developerCreate or select a developer app
Webhook endpoints and signing secrets belong to a developer app and environment. Use a
test app for test integrations and a live app for production activity.
Register an endpoint
POST /api/developer/webhook-endpoints
Content-Type: application/json{
"name": "Production events",
"developer_app_id": "b7a4e408-32e5-4fae-92ac-fbf63ca40934",
"url": "https://example.com/webhooks/cloove",
"environment": "live",
"events": [
"order.created",
"order.updated",
"payment.received"
]
}Retrieve the signing secret
POST /api/developer/webhook-settings/live/view-secret
Content-Type: application/json{
"developer_app_id": "b7a4e408-32e5-4fae-92ac-fbf63ca40934"
}Viewing or rotating a signing secret requires recent password verification. Store the
returned whsec_... value in your server-side secret manager.
Verify and acknowledge deliveries
Verify the signature against the raw body, persist the event ID, enqueue your own work,
and return a 2xx response before the 10-second timeout.
Endpoint rules
Endpoint resource
{
"id": "b27a432e-c8dd-46a8-9e9f-75d740f5d716",
"businessId": "6cfef582-cbd0-4755-9c65-53dcaea00e64",
"developerAppId": "b7a4e408-32e5-4fae-92ac-fbf63ca40934",
"name": "Production events",
"url": "https://example.com/webhooks/cloove",
"environment": "live",
"events": ["order.created", "order.updated", "payment.received"],
"status": "active",
"lastDeliveryStatus": "delivered",
"failureCount": 0,
"lastDeliveredAt": "2026-07-14T10:02:03.000Z",
"disabledAt": null,
"createdAt": "2026-07-01T08:00:00.000Z",
"updatedAt": "2026-07-14T10:02:03.000Z"
}status is either active or disabled. Only active endpoints receive new events.
Manage endpoints
Use developer_app_id as a query parameter on endpoint, setting, and delivery list
requests. Send it in the JSON body when viewing or rotating a secret.
The endpoint’s developer app and environment are immutable. Create another endpoint if either association must change.
Send a test delivery
POST /api/developer/webhook-endpoints/b27a432e-c8dd-46a8-9e9f-75d740f5d716/testThis queues a signed webhook.test event directly to the selected active endpoint. The
endpoint does not need to subscribe to webhook.test. The response is 202 Accepted and
contains the delivery record, which can be monitored through the deliveries API.
{
"type": "webhook.test",
"apiVersion": "v1",
"data": {
"endpointId": "b27a432e-c8dd-46a8-9e9f-75d740f5d716",
"message": "Cloove webhook test delivery"
}
}Request format
Every delivery is an HTTPS POST with Content-Type: application/json.
Headers
Header names are case-insensitive as required by HTTP.
Envelope
{
"id": "5d8848c5-f009-4b5d-9bb2-4e733f32cb6f",
"type": "vox.call.completed",
"apiVersion": "v1",
"businessId": "6cfef582-cbd0-4755-9c65-53dcaea00e64",
"created": "2026-07-14T10:02:02.000Z",
"environment": "live",
"data": {
"callId": "96f6450d-f369-4c27-8688-d8190e60d7f4",
"status": "completed",
"direction": "outbound",
"durationSeconds": 122,
"recordingUrl": null
}
}The id value matches Cloove-Webhook-Id. All subscribed endpoints receive the same
event ID for the same emitted event. Each endpoint still has its own delivery record and
retry lifecycle.
Verify signatures
Never process a delivery until its signature and timestamp have been verified.
Cloove signs these exact bytes:
HMAC-SHA256(signing_secret, unix_timestamp + "." + raw_request_body)The result is encoded as lowercase hexadecimal and placed in v1.
Verify the raw request body exactly as received. Parsing and re-serializing JSON can change whitespace or key ordering and invalidate the signature.
Replay tolerance
Reject signatures whose t value is too far from the current time. Five minutes is a
reasonable default. Timestamp validation limits captured-request replay, while event-ID
deduplication protects normal retries and manual resends.
Node.js
const crypto = require('node:crypto')
function verifyClooveWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
if (!Buffer.isBuffer(rawBody) || !signatureHeader || !secret) return false
const values = new Map()
for (const part of signatureHeader.split(',')) {
const separator = part.indexOf('=')
if (separator > 0) values.set(part.slice(0, separator), part.slice(separator + 1))
}
const timestamp = Number(values.get('t'))
const providedHex = values.get('v1')
if (!Number.isInteger(timestamp) || !/^[a-f0-9]{64}$/i.test(providedHex || '')) {
return false
}
const age = Math.abs(Math.floor(Date.now() / 1000) - timestamp)
if (age > toleranceSeconds) return false
const expectedHex = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(expectedHex, 'hex'),
Buffer.from(providedHex, 'hex')
)
}Framework raw-body handling
In Express, use express.raw({ type: 'application/json' }) on the webhook route before
global JSON parsing. In Next.js route handlers, read await request.arrayBuffer() before
calling JSON.parse. In Python frameworks, use the request body’s byte-oriented API.
Receiver pattern
Your handler should perform only the work required to authenticate and durably accept the event.
app.post('/webhooks/cloove', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.get('Cloove-Signature')
if (!verifyClooveWebhook(req.body, signature, process.env.CLOOVE_WEBHOOK_SECRET)) {
return res.status(400).send('Invalid signature')
}
const event = JSON.parse(req.body.toString('utf8'))
const inserted = await events.insertIfAbsent(event.id, event)
if (inserted) await jobs.enqueue({ eventId: event.id })
return res.status(204).end()
})Use a unique constraint on the event ID. Do not rely on an in-memory set because it is lost when a process restarts and does not coordinate multiple application instances.
Event catalog
The canonical catalog is available from:
GET /api/developer/webhook-eventsReceivers should ignore unknown fields and safely log unknown event types. New fields can
be added to existing payloads without changing apiVersion.
Vox calls and agents
Call event data:
{
"callId": "96f6450d-f369-4c27-8688-d8190e60d7f4",
"status": "completed",
"direction": "outbound",
"durationSeconds": 122,
"recordingUrl": null
}Agent event data:
{
"id": "d94d1454-ff9e-47da-aac8-d1c509f7794b",
"name": "Sales concierge",
"status": "active",
"storeId": null,
"language": "en-NG",
"tone": "professional",
"isDefault": true,
"updatedAt": "2026-07-14T10:00:00.000Z"
}Messaging
Inbound message data:
{
"conversationId": "94d8f967-32bb-4740-a6ea-71da41c401b8",
"messageId": "53c6e4aa-d3d2-45a3-87ab-af8945d0486d",
"customerPhone": "+2348012345678",
"messageType": "text",
"text": "Is my order ready?",
"receivedAt": "2026-07-14T10:00:00.000Z"
}Delivery update data:
{
"messageId": "53c6e4aa-d3d2-45a3-87ab-af8945d0486d",
"conversationId": "94d8f967-32bb-4740-a6ea-71da41c401b8",
"deliveryStatus": "delivered",
"metaMessageId": "wamid.HBg..."
}Orders, payments, contacts, inventory, and wallet
These events originate from Cloove’s business event ledger and share this data shape:
{
"businessId": "6cfef582-cbd0-4755-9c65-53dcaea00e64",
"eventType": "ORDER_CREATED",
"entityType": "sale",
"entityId": "5037c85a-44c6-4c3a-bd7d-3e21e73915e1",
"storeId": "837ceae4-43c2-4bbb-830c-1099d36b9d05",
"metadata": {
"shortCode": "482193",
"totalAmount": 7000
},
"occurredAt": "2026-07-14T10:00:00.000Z"
}metadata varies by operation and can gain fields over time. Use entityId to retrieve
the latest resource when a complete current representation is required. The webhook is a
change notification, not a replacement for resource retrieval.
For a payment.received event created through a payment link, data also includes the
durable payment-link and customer mapping:
{
"businessId": "6cfef582-cbd0-4755-9c65-53dcaea00e64",
"eventType": "PAYMENT_RECEIVED",
"entityType": "sale",
"entityId": "5037c85a-44c6-4c3a-bd7d-3e21e73915e1",
"metadata": {
"amount": 50000,
"paymentMethod": "VIRTUAL_ACCOUNT",
"paymentLinkId": "79b70c64-81f4-4b7c-a72f-b39dd253ea42",
"customerId": "5c2ec758-b919-46b9-82f7-5fcbc2636b66"
},
"paymentLink": {
"id": "79b70c64-81f4-4b7c-a72f-b39dd253ea42",
"reference": "CLV-PL-0001",
"targetType": "wallet",
"targetId": null,
"amount": 50000,
"currency": "NGN"
},
"customer": {
"id": "5c2ec758-b919-46b9-82f7-5fcbc2636b66",
"name": "Ada Lovelace",
"email": "ada@example.com",
"phoneNumber": "2348012345678"
},
"occurredAt": "2026-07-14T10:00:00.000Z"
}Use customer.id as the Cloove customer identifier. Email and phone number are included so
you can reconcile the event with an external customer record. paymentLink.reference identifies
the exact link that collected the payment. Payments that did not originate from a payment link
do not include paymentLink or the payment-link customer block.
There is no order.refunded event in the current public catalog. A database-only status
change is not treated as a refund. Refund events will be introduced with a money-moving
refund workflow.
Products
{
"id": "7a59174f-3207-42a6-8fab-5b8d8b2929fb",
"name": "Jollof Rice",
"type": "simple",
"basePrice": 3500,
"categoryId": "bf0824e4-f6b8-4a47-9ef9-c6bc5dd8c6f4",
"unit": "PLATE",
"isActive": true,
"isExtraOnly": false,
"updatedAt": "2026-07-14T10:00:00.000Z"
}Stock threshold events remain separate from product configuration events. Subscribe to both product and inventory events when maintaining an external catalog with availability.
Hotel
Reservation events contain the same reservation representation described in the Hotel API.
Service request data:
{
"id": "b601cf35-05ef-4574-859b-6fe81807ae6c",
"category": "housekeeping",
"status": "new",
"guestId": "80be51aa-da85-4081-9b7a-9f4f72d0433f",
"reservationId": "20b74d5c-7635-4671-9b0d-3d112a875690",
"roomId": "b0825fc0-e38c-4813-b59d-ef35d96cf9b1",
"source": "dashboard"
}Delivery attempts and retries
A delivery succeeds only when the endpoint returns a status from 200 through 299
within 10 seconds. Response bodies are ignored.
Redirects are not success responses. Configure the final HTTPS URL instead of depending
on a 301, 302, 307, or 308 response.
Each retry uses the same event ID and persisted payload. The signature timestamp and HMAC are generated again for each attempt.
Inspect deliveries
GET /api/developer/webhook-deliveries?developer_app_id=b7a4e408-32e5-4fae-92ac-fbf63ca40934The API returns the latest 100 deliveries in reverse chronological order.
{
"id": "02f5f289-e5d4-42e9-9833-1a49a840e340",
"businessId": "6cfef582-cbd0-4755-9c65-53dcaea00e64",
"developerAppId": "b7a4e408-32e5-4fae-92ac-fbf63ca40934",
"webhookEndpointId": "b27a432e-c8dd-46a8-9e9f-75d740f5d716",
"environment": "live",
"eventId": "5d8848c5-f009-4b5d-9bb2-4e733f32cb6f",
"eventType": "order.created",
"status": "failed",
"url": "https://example.com/webhooks/cloove",
"attempts": 2,
"responseStatus": 500,
"latencyMs": 184,
"errorMessage": "http_500",
"payloadPreview": {
"preview": "{\"id\":\"5d8848c5-f009-4b5d-9bb2-4e733f32cb6f\",..."
},
"nextAttemptAt": "2026-07-14T10:02:10.000Z",
"deliveredAt": null,
"createdAt": "2026-07-14T10:02:02.000Z",
"updatedAt": "2026-07-14T10:02:05.000Z"
}Delivery statuses
failed can be temporary while automatic retries remain. Use attempts,
nextAttemptAt, and the endpoint’s failureCount when diagnosing repeated failures.
Manual resend
POST /api/developer/webhook-deliveries/02f5f289-e5d4-42e9-9833-1a49a840e340/resendManual resend resets the delivery result and queues the persisted payload again. The event ID and JSON payload remain the same. The request receives a new signature timestamp.
The destination endpoint must be active. Enabling an endpoint does not automatically resend its failed deliveries; request resends for the deliveries you still need.
Disable and re-enable behavior
An endpoint is automatically disabled after 15 consecutive failed attempts. It stops receiving new events and queued attempts stop when the worker observes the disabled state.
After fixing the receiver:
- Enable the endpoint with
POST /webhook-endpoints/:id/enable. - Confirm its URL and subscriptions.
- Resend relevant failed deliveries.
- Monitor for a successful delivery, which keeps the failure count at zero.
Rotate a signing secret
POST /api/developer/webhook-settings/live/rotate-secret
Content-Type: application/json{
"developer_app_id": "b7a4e408-32e5-4fae-92ac-fbf63ca40934"
}Rotation replaces the secret for that developer app and environment immediately. New attempts, including retries of older events, are signed with the new secret. There is no automatic overlap period for the old secret.
Store the returned secret securely and update every receiver for the app. Never expose it in browser JavaScript, mobile application bundles, logs, or source control.
Operational checklist
- Verify the signature against raw bytes before parsing.
- Enforce a timestamp tolerance.
- Deduplicate with a database unique constraint on
id. - Persist or enqueue before returning
2xx. - Keep the handler below the 10-second timeout.
- Treat events as unordered.
- Retrieve the current API resource when exact latest state matters.
- Ignore unknown fields and handle unknown event types safely.
- Alert on repeated failures and disabled endpoints.
- Keep test and live secrets separate.
- Rotate a secret immediately if it is exposed.
Troubleshooting
See the Developer Portal API for API key and app management, and API conventions for compatibility rules.