Signed events pushed to your HTTPS endpoint when money lands, cover changes, a certificate is issued, a claim moves or premium finance is decided.
Register HTTPS endpoints and Flexysure will POST a signed JSON event to them when money lands, cover changes, a motor certificate is issued, a claim moves or premium finance is decided. Deliveries are retried after 1 m, 5 m, 30 m, 2 h and 12 h, then marked failed; failed deliveries can be retried by hand. Verify every request with the Flexysure-Signature header: t=<unix>,v1=<hex HMAC-SHA256 of "{t}.{raw body}" with your endpoint secret>; reject timestamps older than 300 seconds and compare in constant time.
Register endpoints on Settings → Webhooks or with the webhooks API (scope webhooks). Each endpoint has its own signing secret, shown once when it is created or rotated.
Event catalogue
Every delivery is a JSON envelope: id (the event id, equal to the Flexysure-Delivery header), event, created_at, api_version, tenant and data. Money uses whole shillings in amount plus cents in amount_minor. Subscribe to * to receive every event, including ones added later.
pingTest eventSettings → Webhooks → Send test event
Sent by the "Send test event" button in Settings → Webhooks. Not in the catalogue; delivered to the chosen endpoint regardless of its subscriptions.
PayloadJSON
{
"id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
"event": "ping",
"created_at": "2026-10-14T09:31:05+03:00",
"api_version": "v1",
"tenant": {
"code": "SIMBA",
"name": "Simba Risk Brokers"
},
"data": {
"message": "Test event from Flexysure. If you can read this, your receiver and secret are set up correctly.",
"sent_by": "Achieng Odhiambo"
}
}
Signatures
Every request carries Flexysure-Signature: t=<unix time>,v1=<signature>. The signature is the hex HMAC-SHA256 of the string {t}.{raw body}, keyed with the endpoint's secret. To verify:
Split the header on commas and read t and every v1. While a secret is being rotated there can be more than one v1; any match is valid.
Reject the request if t is more than 300 seconds from your clock. This stops replays.
Compute the HMAC over the raw bytes you received, before parsing JSON. Re-encoding the JSON changes the bytes and breaks the signature.
Compare in constant time, then deduplicate on Flexysure-Delivery and answer with any 2xx within 10 seconds (the request times out after that). Do the slow work after acknowledging.
Header
Meaning
Flexysure-Signature
t=1760423465,v1=5f2b1c…e9a3 — t is the unix time of signing, v1 the hex HMAC-SHA256 of "{t}.{body}" with the endpoint secret. Several v1 parts may appear during a rotation.
Flexysure-Event
The event name, e.g. payment.received.
Flexysure-Delivery
UUID of the event. Identical on every retry: use it to ignore duplicates.
Content-Type
application/json
User-Agent
Flexysure-Webhooks/1.0
What arrivesHTTP
POST /flexysure/webhooks HTTP/1.1
Content-Type: application/json
User-Agent: Flexysure-Webhooks/1.0
Flexysure-Event: payment.received
Flexysure-Delivery: 6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60
Flexysure-Signature: t=1760423465,v1=<hex HMAC-SHA256 of "1760423465." + body>
{"id":"6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60","event":"payment.received",…}
Retries
A delivery that does not get a 2xx (or times out) is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours: 6 attempts in all. After the last one it is marked failed. Failed deliveries can be sent again from Settings → Webhooks or with the retry endpoint; every retry keeps the same Flexysure-Delivery id.
Attempt 1 · immediately
Attempt 2 · +1 minute
Attempt 3 · +5 minutes
Attempt 4 · +30 minutes
Attempt 5 · +2 hours
Attempt 6 · +12 hours
Failed
Verification samples
Each sample applies the same rules as our own Dispatcher::verify.
PHP
receiver.phpPHP
<?php
function flexysure_verify(string $secret, string $header, string $body, int $tolerance = 300): bool
{
$timestamp = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
$pair = explode('=', trim($part), 2);
if (count($pair) !== 2) {
continue;
}
[$key, $value] = $pair;
if ($key === 't' && ctype_digit($value)) {
$timestamp = (int) $value;
} elseif ($key === 'v1' && preg_match('/^[a-f0-9]{64}$/', $value) === 1) {
$signatures[] = $value;
}
}
if ($timestamp === null || $signatures === []) {
return false;
}
if ($tolerance > 0 && abs(time() - $timestamp) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $timestamp.'.'.$body, $secret);
foreach ($signatures as $candidate) {
if (hash_equals($expected, $candidate)) {
return true;
}
}
return false;
}
// Receiver: verify the raw body (before any JSON decoding), then acknowledge fast.
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_FLEXYSURE_SIGNATURE'] ?? '';
if (! flexysure_verify(getenv('FLEXYSURE_WEBHOOK_SECRET'), $header, $body)) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// Ignore duplicates: $event['id'] equals the Flexysure-Delivery header and is the same on every retry.
http_response_code(200);
Node.js (Express)
receiver.jsJavaScript
const crypto = require('crypto');
const express = require('express');
function verifyFlexysure(secret, header, rawBody, tolerance = 300) {
let timestamp = null;
const signatures = [];
for (const raw of String(header || '').split(',')) {
const part = raw.trim();
const i = part.indexOf('=');
if (i < 0) continue;
const key = part.slice(0, i);
const value = part.slice(i + 1);
if (key === 't' && /^[0-9]+$/.test(value)) timestamp = parseInt(value, 10);
else if (key === 'v1' && /^[a-f0-9]{64}$/.test(value)) signatures.push(value);
}
if (timestamp === null || signatures.length === 0) return false;
if (tolerance > 0 && Math.abs(Math.floor(Date.now() / 1000) - timestamp) > tolerance) return false;
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
return signatures.some((candidate) => crypto.timingSafeEqual(expected, Buffer.from(candidate, 'hex')));
}
const app = express();
// Keep the raw bytes: the signature covers exactly what Flexysure sent.
app.post('/flexysure/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyFlexysure(process.env.FLEXYSURE_WEBHOOK_SECRET, req.get('Flexysure-Signature'), req.body)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body.toString('utf8'));
// Ignore duplicates: event.id equals the Flexysure-Delivery header and is the same on every retry.
res.sendStatus(200);
});
Python (Flask)
receiver.pyPython
import hashlib
import hmac
import os
import re
import time
from flask import Flask, request
def verify_flexysure(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
timestamp = None
signatures = []
for part in (header or "").split(","):
key, sep, value = part.strip().partition("=")
if not sep:
continue
if key == "t" and re.fullmatch(r"[0-9]+", value):
timestamp = int(value)
elif key == "v1" and re.fullmatch(r"[a-f0-9]{64}", value):
signatures.append(value)
if timestamp is None or not signatures:
return False
if tolerance > 0 and abs(int(time.time()) - timestamp) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, candidate) for candidate in signatures)
app = Flask(__name__)
@app.post("/flexysure/webhooks")
def flexysure_webhook():
# request.get_data() is the raw body: verify it before parsing JSON.
if not verify_flexysure(os.environ["FLEXYSURE_WEBHOOK_SECRET"], request.headers.get("Flexysure-Signature", ""), request.get_data()):
return "", 400
event = request.get_json()
# Ignore duplicates: event["id"] equals the Flexysure-Delivery header and is the same on every retry.
return "", 200
Managing endpoints
7 endpoints, scope webhooks, base URL https://flexysure.intrepidprojects.co.ke/api/v1.