# Webhooks

Webhooks allow partners to be notified when important events happen in Offers.

When one of those events are triggered, we will send an HTTP POST payload to the webhook's configured URL. Webhooks can be used to send a customer notification, initiate a policy renewal process or perform any custom logic.

To establish webhooks, partners can provide to their CSE:

1\. Listener URL\
2\. Authentication Key & Secret\
3\. Support URL\
4\. Requested events (CSE may propose or suggest these as part of solutioning).

We will provide an HTTP signature generated on our end in `Authorization` header and the api key itself in `X-Api-Key` header. We will base the signature on your provided key pair. You can use the same `HMAC` based algorithm for signature verification, if required. Please use the information from the signature header to check which hash algorithm is used in order to validate the request. Currently, webhook requests are signed using `sha256` algorithm.

In case of multiple failures with the webhook notification, where the partner supplied endpoint does not respond with a 200 OK, we will try the webhook for up to 3 times.

### Authentication

Your webhook API endpoint should implement HMAC authentication to verify that the API request was sent and signed by Offers.

{% tabs %}
{% tab title="Python" %}
{% code expandable="true" %}

```python
import base64
import hashlib
import hmac
from urllib.parse import unquote

# You will provide your assigned Client Solution Engineer with an API key and secret
# they will configure the Offers API
api_key = "--your-api-key--"
secret = "--your-secret--"
# This is just an example, you would obtain this from your server library
# E.g. a Flask server
# from flask import Flask, request
# app = Flask(__name__)
# @app.post('/my-offers-api-webhook')
# def offers_api_webhook():
#.   request_headers = request.headers 
#    return do_everything_below()

request_headers = {
    'X-Api-Key': '--api-key--',
    'Authorization': '--signature--',
    'Date': 'Thu, 27 Feb 2025 05:01:47 GMT'
}

# Extract signature from the request headers
auth_header = request_headers.get('Authorization', '')
if not auth_header.startswith('Signature '):
    raise ValueError("Invalid Authorization header")

auth_parts = dict(
    part.split('=', 1) for part in 
    auth_header.replace('Signature ', '').split(',')
)
received_signature = unquote(auth_parts.get('signature', '').strip('"'))

# Compute the expected signature
date_header = request_headers.get('Date')
if not date_header:
    raise ValueError("Missing Date header")

raw = f"date: {date_header}"

expected_hash = hmac.new(
    secret.encode("utf-8"),
    raw.encode("utf-8"),
    hashlib.sha256
).digest()

expected_signature = base64.b64encode(expected_hash).decode('utf-8')

is_valid = hmac.compare_digest(expected_signature, received_signature)
```

{% endcode %}
{% endtab %}

{% tab title="JavaScript" %}
{% code overflow="wrap" expandable="true" %}

```javascript
const crypto = require('crypto');

// You will provide your assigned Client Solution Engineer with an API key and secret
// they will configure the Offers API
const apiKey = "--your-api-key--";
const secret = "--your-secret--";

// This is just an example, you would obtain this from your server library
// E.g. an Express server
// const express = require('express');
// const app = express();
// app.post('/my-offers-api-webhook', (req, res) => {
//   const requestHeaders = req.headers;
//   return doEverythingBelow();
// });

const requestHeaders = {
  'X-Api-Key': '--api-key--',
  'Authorization': '--signature--',
  'Date': 'Thu, 27 Feb 2025 05:01:47 GMT'
};

// Extract signature from the request headers
const authHeader = requestHeaders['Authorization'] || '';
if (!authHeader.startsWith('Signature ')) {
  throw new Error("Invalid Authorization header");
}

// Parse the Authorization header
const authString = authHeader.replace('Signature ', '');
const authParts = {};
authString.split(',').forEach(part => {
  const [key, value] = part.split('=', 2);
  authParts[key.trim()] = value;
});

const receivedSignature = decodeURIComponent(
  (authParts['signature'] || '').replace(/^"|"$/g, '')
);

// Compute the expected signature
const dateHeader = requestHeaders['Date'];
if (!dateHeader) {
  throw new Error("Missing Date header");
}

const raw = `date: ${dateHeader}`;

// Create HMAC-SHA256 hash
const expectedHash = crypto
  .createHmac('sha256', secret)
  .update(raw, 'utf8')
  .digest();

const expectedSignature = expectedHash.toString('base64');

// Timing-safe comparison
const isValid = crypto.timingSafeEqual(
  Buffer.from(expectedSignature),
  Buffer.from(receivedSignature)
);

console.log('Is valid:', isValid);
```

{% endcode %}
{% endtab %}

{% tab title="PHP" %}

```php
<?php

// You will provide your assigned Client Solution Engineer with an API key and secret
// they will configure the Offers API
$apiKey = "--your-api-key--";
$secret = "--your-secret--";

// This is just an example, you would obtain this from your server library
// E.g. a Laravel controller
// public function offersApiWebhook(Request $request)
// {
//     $requestHeaders = $request->headers->all();
//     return $this->doEverythingBelow();
// }

$requestHeaders = [
    'X-Api-Key' => '--api-key--',
    'Authorization' => '--signature--',
    'Date' => 'Thu, 27 Feb 2025 05:01:47 GMT'
];

// Extract signature from the request headers
$authHeader = $requestHeaders['Authorization'] ?? '';
if (!str_starts_with($authHeader, 'Signature ')) {
    throw new InvalidArgumentException("Invalid Authorization header");
}

// Parse the Authorization header
$authString = str_replace('Signature ', '', $authHeader);
$authParts = [];
foreach (explode(',', $authString) as $part) {
    [$key, $value] = explode('=', $part, 2);
    $authParts[trim($key)] = $value;
}

$receivedSignature = urldecode(
    trim($authParts['signature'] ?? '', '"')
);

// Compute the expected signature
$dateHeader = $requestHeaders['Date'] ?? null;
if (!$dateHeader) {
    throw new InvalidArgumentException("Missing Date header");
}

$raw = "date: {$dateHeader}";

// Create HMAC-SHA256 hash
$expectedHash = hash_hmac('sha256', $raw, $secret, true);
$expectedSignature = base64_encode($expectedHash);

// Timing-safe comparison
$isValid = hash_equals($expectedSignature, $receivedSignature);

var_dump($isValid);
```

{% endtab %}
{% endtabs %}

### BOOKING\_CREATED

*Description*: The payload is sent in a webhook to the Partner whenever the Offer is Confirmed through an API Request.

```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
      }
    ]
  }
}
```

### BOOKING\_UPDATED

*Description*: The payload is sent to the Partner whenever the Confirmed Offer is Updated through an API Request

```json
{
  "event": "BOOKING_UPDATED",
  "payload": {
    "id": "TKBZN-XGPAX-INS",
    "status": "CONFIRMED",
    "currency": "USD",
    "total_price": 68.71,
    "total_price_formatted": "US$68.71",
    "partner_transaction_id": null,
    "quotes": [
      {
        "id": "4d3b7919-9372-4c54-bdaf-05f770ffc2fc",
        "policy_start_date": "2023-10-08T01:09:19.649112+00:00",
        "policy_end_date": "2023-10-13T01:09:19.649133+00:00",
        "status": "CONFIRMED",
        "price": 68.71,
        "price_formatted": "US$68.71",
        "policy": {
          "policy_type": "travel_medical",
          "policy_name": "PolicyVersion 0",
          "policy_version": "1637bc92-0299-4624-8f38-c9b9e15d379e"
        },
        "duration": "5 00:00:00.000021",
        "total_renewed_times": 0
      }
    ]
  }
}  
```

### BOOKING\_CANCELLED

*Description*: The payload is sent to the Partner whenever the Confirmed Offer is cancelled through an API Request

```json
{
  "event": "BOOKING_CANCELLED",
  "payload": {
    "id": "HMZZ8-3YYHZ-INS",
    "status": "CANCELLED",
    "currency": "USD",
    "total_price": 0.0,
    "total_price_formatted": "US$0.00",
    "partner_transaction_id": null,
    "quotes": [
      {
        "id": "e0925616-824d-4216-bd18-450fb669c017",
        "policy_start_date": "2023-10-08T01:19:41.766661+00:00",
        "policy_end_date": "2023-10-13T01:19:41.766719+00:00",
        "status": "CANCELLED",
        "price": 68.71,
        "price_formatted": "US$68.71",
        "policy": {
          "policy_type": "travel_medical",
          "policy_name": "PolicyVersion 0",
          "policy_version": "dd5aef78-5ce5-4f19-a455-968780b89ea2"
        },
        "duration": "5 00:00:00.000058",
        "total_renewed_times": 0,
        "refund_value": 68.71
      }
    ]
  }
}
```

### RENEWAL\_CREATED

*Description*: Used to notify partner that the policy has been renewed.

*Example*:

```json
{
    "event": "RENEWAL_CREATED",
    "payload": {
        "id": "daSJp-fwdoj-REN",
        "package_id": "GKDK9-CECQU-INS",
        "quote_id": "c8ddb161-e3d9-455b-9621-96df90f17acb",
        "status": "ACTIVE",
        "start_date": "2019-02-25T13:00:00Z",
        "notification_date": "2018-11-22T00:21:41Z",
        "due_date": "2019-02-25T13:00:00Z",
        "expiry_date": "2018-11-21T22:56:01Z",
        "cancelled_on": null,
        "paid_on": "2019-02-25T13:00:00Z",
        "created_at": "2018-11-21T04:15:12.175468Z"
    }
}
```

### RENEWAL\_NOTIFICATION

*Description*: Used to notify partner about approaching renewal.

*Example*:

```json
{
    "event": "RENEWAL_NOTIFICATION",
    "payload": {
        "id": "daSJp-fwdoj-REN",
        "package_id": "GKDK9-CECQU-INS",
        "quote_id": "c8ddb161-e3d9-455b-9621-96df90f17acb",
        "status": "ACTIVE",
        "start_date": "2019-02-25T13:00:00Z",
        "notification_date": "2018-11-22T00:21:41Z",
        "due_date": "2019-02-25T13:00:00Z",
        "expiry_date": "2018-11-21T22:56:01Z",
        "cancelled_on": null,
        "paid_on": null,
        "created_at": "2018-11-21T04:15:12.175468Z"
    }
}
```

### RENEWAL\_DUE

*Description*: Sent on the renewal due date, the customer policy will be active until the end of the grace period (`expiry_date`).

*Example*:

```json
{
    "event": "RENEWAL_DUE",
    "payload": {
        "id": "daSJp-fwdoj-REN",
        "package_id": "GKDK9-CECQU-INS",
        "quote_id": "c8ddb161-e3d9-455b-9621-96df90f17acb",
        "status": "DUE",
        "start_date": "2019-02-25T13:00:00Z",
        "notification_date": "2018-11-22T00:21:41Z",
        "due_date": "2019-02-25T13:00:00Z",
        "expiry_date": "2018-11-21T22:56:01Z",
        "cancelled_on": null,
        "paid_on": null,
        "created_at": "2018-11-21T04:15:12.175468Z"
    }
}
```

### RENEWAL\_EXPIRED

*Description*: Sent on the renewal expiry date, the customer policy is no longer renewable after this event is triggered.

*Example*:

```json
{
    "event": "RENEWAL_DUE",
    "payload": {
        "id": "daSJp-fwdoj-REN",
        "package_id": "GKDK9-CECQU-INS",
        "quote_id": "c8ddb161-e3d9-455b-9621-96df90f17acb",
        "status": "DUE",
        "start_date": "2019-02-25T13:00:00Z",
        "notification_date": "2018-11-22T00:21:41Z",
        "due_date": "2019-02-25T13:00:00Z",
        "expiry_date": "2018-11-21T22:56:01Z",
        "cancelled_on": null,
        "paid_on": null,
        "created_at": "2018-11-21T04:15:12.175468Z"
    }
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://partner-docs.covergenius.com/offers/api/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
