Guide to Cover Genius Webhooks: Features and Best Practices
Cover Genius webhooks tell a partner what has happened to the insurance it sells through Cover Genius: a booking is confirmed, changed or cancelled, or a policy comes up for renewal. If you embed XCover protection in your checkout, webhooks are how those changes reach your customer notifications, refund handling and renewal flows without polling the Offers API.
This guide covers how Offers webhooks are set up and authenticated, the seven events and their payloads, the limits of Cover Genius's delivery model, and the best practices for running a receiver in production.
What are Cover Genius webhooks?
Cover Genius has three partner products that send webhooks, and each authenticates them differently:
- Offers (XCover bookings and renewals) sends your authentication key in an
X-Api-Keyheader. - XClaim (claims) signs the body in an
XClaim-Signatureheader. - RentalCover signs the body and a timestamp in an
X-Signatureheader.
This guide covers Offers, which is the product Hookdeck's Cover Genius source type supports. An Offers webhook is an HTTP POST with a JSON body of the form { "event": "...", "payload": { ... } }. The body is not signed. Cover Genius proves the request is from them by sending the key you gave them, as plain text, in X-Api-Key, and your endpoint compares it with its stored copy.
Offers webhooks also carry an Authorization: Signature ... header. It is an HMAC-SHA256 over the string date: <Date header>, keyed with your secret, so it covers the Date header and nothing else. It confirms the sender knows your secret but says nothing about the body, so it adds little over the key check.
Cover Genius webhook features
| Feature | Details |
|---|---|
| Configuration | Not self-serve. Your Cover Genius Client Solution Engineer (CSE) sets it up |
| Authentication | Your key, as plain text with no prefix, in X-Api-Key |
| Additional header | Authorization: Signature ..., an HMAC-SHA256 over date: <Date header> only |
| Body signature | None |
| Envelope | { "event", "payload" } |
| Routing | The body's event field |
| Events | 7 booking and renewal events |
| Event ID | None |
| Success response | 200 |
| Retries | Up to 3 attempts in total. No timeout or spacing documented |
| IP allowlist | None published |
Common events
| Event | Sent when |
|---|---|
BOOKING_CREATED | An Offer is confirmed through an API request |
BOOKING_UPDATED | A confirmed Offer is updated through an API request |
BOOKING_CANCELLED | A confirmed Offer is cancelled through an API request |
RENEWAL_CREATED | The policy has been renewed |
RENEWAL_NOTIFICATION | A renewal is approaching |
RENEWAL_DUE | The renewal due date arrives. The policy stays active until the end of the grace period (expiry_date) |
RENEWAL_EXPIRED | The renewal expiry date arrives, and the policy can no longer be renewed |
Booking events carry the booking under payload, with its quotes. Here is Cover Genius's BOOKING_CREATED example:
{
"event": "BOOKING_CREATED",
"payload": {
"id": "8AMKH-KQ8NR-INS",
"status": "CONFIRMED",
"currency": "USD",
"total_price": 68.71,
"total_price_formatted": "US$68.71",
"partner_transaction_id": null,
"quotes": [
{
"id": "8bfb48ab-6947-4f34-a932-c4dcede958f6",
"policy_start_date": "2023-10-08T00:00:49.751227+00:00",
"policy_end_date": "2023-10-13T00:00:49.751247+00:00",
"status": "CONFIRMED",
"price": 68.71,
"price_formatted": "US$68.71",
"policy": {
"policy_type": "travel_medical",
"policy_name": "PolicyVersion 0",
"policy_version": "e94df011-ee0b-44a1-bdb1-1dd05cb67fc7"
},
"duration": "5 00:00:00.000020",
"total_renewed_times": 0
}
]
}
}
On BOOKING_CANCELLED, status is CANCELLED and each quote adds refund_value. Renewal events carry a renewal object instead, with id, package_id, quote_id, status, start_date, notification_date, due_date, expiry_date, cancelled_on, paid_on and created_at.
There is no event ID or delivery timestamp in the envelope. The closest thing to a unique key is the booking or renewal id combined with event.
Setting up Cover Genius webhooks
There is no dashboard for Offers webhooks. You send the details to your CSE, who configures them:
- Listener URL. Your HTTPS endpoint, for example
https://example.com/webhooks/cover-genius. - Authentication Key and Secret. Generate a long random key (for example with
openssl rand -hex 32) and a secret. Cover Genius sends the key inX-Api-Keyand uses the secret for theAuthorizationsignature. - Support URL.
- Requested events. Choose from the seven above. Your CSE may suggest events as part of solutioning.
Keep the key in your server's environment, for example as COVER_GENIUS_API_KEY. If you receive Offers webhooks through Hookdeck, enter the same key in the Hookdeck source. Only Offers webhooks, which send the key in X-Api-Key, are supported by the Cover Genius source type. XClaim and RentalCover deliveries do not send that header and fail the check.
To test your handler before your CSE turns anything on, send a request yourself:
curl -X POST http://localhost:3000/webhooks/cover-genius \
-H "Content-Type: application/json" \
-H "X-Api-Key: $COVER_GENIUS_API_KEY" \
-d '{"event":"BOOKING_CANCELLED","payload":{"id":"HMZZ8-3YYHZ-INS","status":"CANCELLED","quotes":[]}}'
Securing Cover Genius webhooks
Read X-Api-Key, compare it with your key in constant time, and reject the request if it is missing or different. Fail closed if your key is not configured, so an empty header can never match an empty variable. The body plays no part in the check, but authenticate before you parse it.
const crypto = require('crypto');
const express = require('express');
function verifyCoverGeniusKey(headerValue, apiKey) {
if (typeof headerValue !== 'string' || !apiKey) return false;
const received = Buffer.from(headerValue, 'utf8');
const expected = Buffer.from(apiKey, 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
const app = express();
app.post('/webhooks/cover-genius', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyCoverGeniusKey(req.get('x-api-key'), process.env.COVER_GENIUS_API_KEY)) {
return res.sendStatus(401);
}
const { event, payload } = JSON.parse(req.body.toString('utf8'));
// Route on event, deduplicate on event + payload.id, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
def verify_cover_genius_key(header_value, api_key) -> bool:
if not header_value or not api_key:
return False
return hmac.compare_digest(header_value.encode("utf-8"), api_key.encode("utf-8"))
# verify_cover_genius_key(request.headers.get("x-api-key"), os.environ["COVER_GENIUS_API_KEY"])
You can also check the Authorization signature: URL-decode its signature parameter, compute a base64 HMAC-SHA256 of date: followed by the Date header with your secret, and compare. Because it covers only the Date header, a captured request can be replayed with its original signature, and the body can be altered without breaking it. Treat it as optional.
Make Cover Genius webhooks production-ready. Hookdeck Event Gateway checks the
X-Api-Keyheader on every Offers webhook, keeps each booking and renewal event in a durable queue, and retries delivery to your service on a schedule you control.
Cover Genius webhook limitations and pain points
A shared key, not a signature
The Problem: Nothing ties an Offers webhook's body to Cover Genius. Anyone who learns your key can send a request that passes the check.
Why It Happens: Offers webhooks authenticate with a static key in X-Api-Key. The Authorization signature covers only the Date header.
Workarounds:
- Use an HTTPS listener URL so the key is encrypted in transit, and never log the header.
- Treat the webhook as a notification for high-value actions, and confirm the booking through the Offers API before you issue a refund.
- Rotate the key through your CSE if you suspect it has leaked.
How Hookdeck Can Help: Hookdeck checks X-Api-Key on every request and rejects any that fail with a VERIFICATION_FAILED cause, so forged requests never become events for your service, and the key is checked in one place rather than in each consumer.
Three attempts, then the event is gone
The Problem: A short outage on your side can mean a cancellation or renewal is never delivered.
Why It Happens: Cover Genius tries a webhook up to 3 times when it does not receive a 200, and documents no timeout, spacing or redelivery option beyond that.
Workarounds:
- Return
200as soon as the key check passes, and do the work afterwards. - Reconcile bookings against the Offers API on a schedule, so a missed webhook is caught.
How Hookdeck Can Help: Hookdeck acknowledges each delivery as soon as it arrives and stores it, then retries against your service with automatic retries you configure, so your downtime does not use up Cover Genius's three attempts.
No event ID
The Problem: A retried webhook looks exactly like the original, so a handler can process the same cancellation twice.
Why It Happens: The envelope is only event and payload. There is no delivery or event identifier.
Workarounds:
- Build an idempotency key from
event,payload.idand the field that changes for that event, such asstatus. - Make refund and notification steps safe to run twice.
How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries based on the body fields you choose, before they reach your handler.
A documentation error in the renewal examples
The Problem: Code written from the published RENEWAL_EXPIRED example routes it as RENEWAL_DUE.
Why It Happens: Cover Genius's example for RENEWAL_EXPIRED carries "event": "RENEWAL_DUE". It is a copy of the RENEWAL_DUE example, so the real RENEWAL_EXPIRED body is undocumented.
Workarounds:
- Route on the
eventvalue you actually receive, and log the firstRENEWAL_EXPIREDdelivery in full. - Acknowledge unknown event names with
200rather than an error.
How Hookdeck Can Help: Hookdeck keeps the headers and body of every request, so the first real RENEWAL_EXPIRED delivery becomes a fixture you can inspect and replay into your handler.
Best practices
Compare the key in constant time and fail closed
Use crypto.timingSafeEqual or hmac.compare_digest, check lengths first in Node, and reject everything if the key is not configured. The header value is the bare key, so do not strip a Bearer or other prefix.
Route on event and ignore what you do not handle
Switch on the body's event field. Answer 200 to events you do not process, so a new event never turns into a failed delivery that uses up retries.
Acknowledge fast and process asynchronously
Verify, store the event, return 200, and run refunds and customer messages in the background. See why to process webhooks asynchronously.
Make handlers idempotent
With no event ID, deduplicate on event plus the booking or renewal id and its status. See our guide to webhook idempotency.
Keep XClaim and RentalCover separate
If you also use XClaim or RentalCover, give each product its own endpoint and its own verification. Their signatures cover the body and work differently from the Offers key check.
Conclusion
Cover Genius Offers webhooks arrive as { event, payload } bodies with your key in the X-Api-Key header and no body signature. Compare the key in constant time, route on event, answer 200 quickly because there are only three attempts, and build your own idempotency key because there is no event ID.
Hookdeck Event Gateway checks the X-Api-Key header for you, stores every booking and renewal event, and retries delivery on your terms, so an outage on your side does not lose a cancellation.
Get started with Hookdeck for free and handle Cover Genius webhooks reliably in minutes.