# 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-Key` header.
* **XClaim** (claims) signs the body in an `XClaim-Signature` header.
* **RentalCover** signs the body and a timestamp in an `X-Signature` header.

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:

```json
{
  "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:

1. **Listener URL.** Your HTTPS endpoint, for example `https://example.com/webhooks/cover-genius`.
2. **Authentication Key and Secret.** Generate a long random key (for example with `openssl rand -hex 32`) and a secret. Cover Genius sends the key in `X-Api-Key` and uses the secret for the `Authorization` signature.
3. **Support URL.**
4. **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:

```bash
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.

```javascript
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:

```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](/event-gateway) checks the `X-Api-Key` header 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 `200` as 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](/docs/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.id` and the field that changes for that event, such as `status`.
* Make refund and notification steps safe to run twice.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/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 `event` value you actually receive, and log the first `RENEWAL_EXPIRED` delivery in full.
* Acknowledge unknown event names with `200` rather 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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### 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](/webhooks/guides/implement-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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Cover Genius webhooks reliably in minutes.