Callback notifications

These are callbacks are made from Ensuro's system back to partner's systems to notify about events on policies. It's mainly used for asynchronous policy creation/resolution via our API.

To get notifications from our system you will need to provide an HTTPS endpoint that will receive the notifications.

Retry policy

The endpoint must reply with a status code of 200. Any response status 4xx or 5xx will be considered a rejected message and will be retried later. If the endpoint does not reply within 10 seconds the message will be considered rejected and retried again later.

The first retry is done after 30 seconds, then doubled each time the request fails up to a max delay of 10 minutes. After 10 retries the notification will not be retried anymore.

Robustness principle and idempotency

You must implement an idempotent consumer to receive notifications from our system.

This means that we guarantee at-least-once delivery, but provide no guarantees about:

  • Repeated messages: your system must gracefully handle receiving the same notification twice. For instance, if you receive a policy/resolved notification twice, you can just ignore the second notification by returning a 200 status code without further processing.

  • Out of order messages: because of the retry policy, you may receive messages in a different order than they were generated. Your system must not expect notification messages in a specific order.

Besides idempotency, you should apply the robustness principle when designing your events receiver:

Be conservative in what you send, be liberal in what you accept

We may add new fields to the notification payload as the protocol evolves, your system should pick what it needs from the payload and simply ignore everything else, instead of failing if a new field is added.

This, of course, only refers to backwards compatible changes like adding new fields. Any backwards-incompatible changes will be rolled out with previous coordination to avoid breaking stuff.

Notification signature

Notifications will be signed by Ensuro. You should verify this signature to make sure the notification really comes from Ensuro. No other authentication method is supported.

To verify the signature you'll need to recalculate the signature on your end and verify that it matches the signature present in the X-Ensuro-Signature header. Here's how to do that:

import hmac
from hashlib import sha256


def check_signature(raw_body: bytes, signature: str, sign_secret: str) -> bool:
    """Check HMAC signature

    Arguments:
      - raw_body: the signed message as bytes
      - signature: the signature received along with the message
      - sign_secret: the secret used for signing

    Returns: True if signature checks out, False otherwise.

    Examples:
    >>> check_signature(b"hello world", "500f38dc7f0b1b86b6911e95cb1ad56bb13409937302e1c0f31f5ab1c397d5b6", "T0pS3cret")
    True

    >>> check_signature(b"hello world", "ff73b9fbfcd2454daa91ad3c232c65090713b18651cb5c0c4f39d57ccc87d4bb", "T0pS3cret")
    False
    """
    calculated_digest = hmac.new(bytes(sign_secret, "utf-8"), msg=raw_body, digestmod=sha256).hexdigest()

    return calculated_digest.lower() == signature.lower()

You must take the raw body as received from the network, without any additional decoding or processing. This is a common error in NodeJS:

const express = require("express");
const app = express();
const port = 3000;

const SIGN_SECRET = "T0pS3cret";

app.use(express.json());

app.post("/notify", (req, res) => {
  const signature = req.get("X-Ensuro-Signature");
  const body = JSON.stringify(req.body); // BAD, don't do this

  if (!checkSignature(body, signature, SIGN_SECRET)) {
    res.status(403).send("Invalid signature");
    return;
  }

  // Process the request
});

app.listen(port, () => {
  console.log(`App started on port ${port}`);
});

By first decoding the JSON payload and then encoding it again as JSON you may be modifying the body and the signature will not match. You should instead do something like this:

const express = require("express");
const app = express();
const port = 3000;

const SIGN_SECRET = "T0pS3cret";

app.use(
  express.json({
    verify: (req, res, buf) => {
      if (!checkSignature(buf, req.get("X-Ensuro-Signature"), SIGN_SECRET)) {
        throw new Error("Invalid signature");
      }
    },
  })
);

app.post("/notify", (req, res) => {
  // Process the request

  res.send("OK");
});

app.listen(port, () => {
  console.log(`App started on port ${port}`);
});

If you aren't using ExpressJS you should check your framework's documentation to find out how to get the raw body of the request for signature validation.

Notifications

Error notifications will have JSON content with the following structure:

{
    "type": ...
    "data": ...
    "error_detail": ...
}

Success notifications will have the following structure:

{
    "type": ...
    "policy": ...
}

Policy Creation

When a policy is successfully created a notification like this one will be sent:

{
    "type": "policy/creation",
    "policy": {
        "actual_payout": null,
        "custom_data": {},
        "ensuro_commission": "9.333260",
        "ensuro_id": "55290711940124341656440333382658655728151858290221369410762348674130909154134",
        "events": [
            {
                "event_type": "creation",
                "log_index": 1,
                "timestamp": "2023-06-28T22:00:00",
                "tx_hash": "0xa1d76042cee072e73778122da8191aeb69c029cd780b67623281fac295945bc7"
            }
        ],
        "expiration": "2023-07-01T19:00:01Z",
        "id": 12,
        "jr_coc": "0.373269",
        "jr_scr": "67.713540",
        "loss_prob": "0.098000000000000000",
        "partner_commission": "0.000000",
        "payout": "1000.200000",
        "premium": "142.363804",
        "pure_premium": "132.326460",
        "quote": null,
        "rm": "/api/riskmodules/0x7A3D6f180ABDAA8C35949a48Cd590bA1c06CDF33/",
        "sr_coc": "0.330815",
        "sr_scr": "120.024000",
        "start": "2023-06-28T22:15:40Z",
        "status": "active",
        "url": "/api/policies/3352/"
    }
}

Note the events array that holds the creation transaction event.

Policy resolution

When a policy is successfully resolved a notification like this one will be sent:

{
    "type": "policy/resolution",
    "policy": {
        "actual_payout": "71.350000",
        "custom_data": {},
        "ensuro_commission": "9.333260",
        "ensuro_id": "55290711940124341656440333382658655728151858290221369410762348674130909154134",
        "events": [
            {
                "event_type": "creation",
                "log_index": 1,
                "timestamp": "2023-06-28T22:00:00",
                "tx_hash": "0xa1d76042cee072e73778122da8191aeb69c029cd780b67623281fac295945bc7"
            },
            {
                "event_type": "resolution",
                "log_index": 2,
                "timestamp": "2023-06-28T22:15:00Z",
                "tx_hash": "0x40652b64a9f82212c3113f78f9b47c54a7c261495877d6176585b8b160d99632"
            }
        ],
        "expiration": "2023-07-01T19:14:45Z",
        "id": 13,
        "jr_coc": "0.373269",
        "jr_scr": "67.713540",
        "loss_prob": "0.098000000000000000",
        "partner_commission": "0.000000",
        "payout": "1000.200000",
        "premium": "142.363804",
        "pure_premium": "132.326460",
        "quote": null,
        "rm": "/api/riskmodules/0x7A3D6f180ABDAA8C35949a48Cd590bA1c06CDF33/",
        "sr_coc": "0.330815",
        "sr_scr": "120.024000",
        "start": "2023-06-28T22:15:40Z",
        "status": "customer_won",
        "url": "/api/policies/3352/"
    }
}

Note the events array that now holds the resolution transaction. Another important field to notice is the actual_payout which holds the amount for which the policy was resolved, as opposed to the payout field that holds the policy's original max payout.

Failed policy creation notification

When a policy's creation has an unrecoverable error a notification like the following will be sent:

{
    "type": "policy/creation/failed",
    "error_detail": "execution reverted: Policy exceeds max duration",
    "data": {
        "quote": {
            "data": {
                "internalId": "78fb131b-95f9-4d9d-bc20-f29ea1952631"
            },
            "data_hash": "0x5bd832fcafbbbc0c96769b60bc52aaaece5a3f79b02255c94f1aaf2806385e6a",
            "ensuro_id": "55290711940124341656440333382658655728151858290221369410762348674130909154134",
            "loss_prob": "0.098",
            "payout": "1000.2",
            "policy_expiration": 1735335367,
            "premium": null,
            "quote_id": "78fb131b-95f9-4d9d-bc20-f29ea1952631",
            "risk_module": "0x7a3d6f180abdaa8c35949a48cd590ba1c06cdf33",
            "valid_until": 1688071049
        },
        "signature": {
            "hash": "0xe78aceeecf645ce31115a891f2386a4c91f11a40526bfe6c829846cb5d6ce829",
            "r": "0xb244fb8ac61181044de43332dc5aae339aa568c474070d4fbf7491837b432777",
            "vs": "0xd168c60a655ae7c6d64229ca466de342cab5308fe257b9a360039acd103bd0f0"
        }
    }
}

The data is the same that you received when calling the new-policy endpoint to request the policy creation.

Failed policy resolution notification

When a policy's resolution has an unrecoverable error a notification like the following will be sent:

{
    "type": "policy/resolution/failed",
    "error_detail": "execution reverted: Policy not found",
    "data": {
        "ensuro_id": "55290711940124341656440333382658655728151858290221369410762348674130909154134",
        "payout": "10000",
        "policy": {
            "id": 12,
            "url": "https://offchain-v2.ensuro.co/api/policies/12/",
            "ensuro_id": "55290711940124341656440333382658655728151858290221369410762348674130909154134",
            "rm": "https://offchain-v2.ensuro.co/api/riskmodules/0x7A3D6f180ABDAA8C35949a48Cd590bA1c06CDF33/",
            "quote": null,
            "premium": "2.191125",
            "payout": "28.980000",
            "loss_prob": "0.052000000000000000",
            "jr_scr": "2.022804",
            "sr_scr": "4.057200",
            "pure_premium": "2.034396",
            "ensuro_commission": "0.143710",
            "partner_commission": "0.000000",
            "jr_coc": "0.006500",
            "sr_coc": "0.006519",
            "start": "2023-06-27T19:00:10Z",
            "expiration": "2023-06-29T12:00:00Z",
            "status": "customer_won",
            "actual_payout": "28.980000",
            "events": [
                {
                    "tx_hash": "0xa1d76042cee072e73778122da8191aeb69c029cd780b67623281fac295945bc7",
                    "timestamp": "2023-06-27T19:47:10Z",
                    "event_type": "creation",
                    "log_index": 11
                },
                {
                    "tx_hash": "0x40652b64a9f82212c3113f78f9b47c54a7c261495877d6176585b8b160d99632",
                    "timestamp": "2023-06-28T18:35:50Z",
                    "event_type": "resolution",
                    "log_index": 3
                }
            ]
        }
    }
}

In this example the policy resolution failed because the policy is already resolved.

Last updated