Skip to content
Demo environment. M-Pesa and DMVIC are simulated; no real money or certificates move. Sandbox details
FlexySure

Webhooks

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.

EventWhen
payment.received A receipt was recorded: M-Pesa STK, paybill (C2B), bank or cash. Fires before allocation, so allocated_minor may still be 0.
policy.issued A policy record exists and is awaiting payment (status pending_payment). The vehicle may not be attached yet; policy.activated carries it.
policy.activated Cover went live: the first debit note was settled (cash) or Flexysure Finance disbursed the loan.
policy.endorsed An endorsement was issued on a policy (premium delta positive, negative or zero).
policy.cancelled A policy was cancelled (client request, non-payment of premium finance, or other).
policy.renewal_due An active policy entered a renewal window. Fired by the daily job at 60, 30 and 7 days before end_on.
certificate.issued DMVIC issued the motor certificate. verify_url opens the public roadside check.
certificate.cancelled A motor certificate was cancelled at DMVIC (policy cancelled, vehicle removed, non-payment).
claim.registered A claim was registered on a policy.
claim.updated A claim changed status. previous_status tells you where it came from.
prospect.created A prospect was added to the CRM pipeline (web form, API or staff).
ipf.application.decided Flexysure Finance approved or declined a premium-finance application. decision is approved or declined.
ipf.loan.disbursed Flexysure Finance paid the underwriter; the loan is active and the policy is live. Includes the repayment schedule.
ipf.instalment.missed A premium-finance instalment is overdue and the loan moved into arrears. overdue lists every unpaid, past-due instalment.
message.sent An SMS or email left the outbox (status sent or delivered).
ping Sent by the "Send test event" button in Settings → Webhooks. Not in the catalogue; delivered to the chosen endpoint regardless of its subscriptions.
payment.received Receipt created

A receipt was recorded: M-Pesa STK, paybill (C2B), bank or cash. Fires before allocation, so allocated_minor may still be 0.

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "payment.received",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "receipt": {
            "number": "RCT-000311",
            "channel": "mpesa_stk",
            "external_ref": "QKX7H2M9PL",
            "status": "unallocated",
            "payer_phone": "254712345678",
            "received_at": "2026-10-14T09:31:04+03:00",
            "amount": 30175,
            "amount_minor": 3017500,
            "allocated_minor": 0
        },
        "client": {
            "client_no": "C000123",
            "name": "Wanjiru Kamau",
            "type": "individual",
            "phone": "0712345678",
            "email": "wanjiru@example.co.ke"
        }
    }
}
policy.issued Policy created

A policy record exists and is awaiting payment (status pending_payment). The vehicle may not be attached yet; policy.activated carries it.

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "policy.issued",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "policy": {
            "uuid": "0d7b1f1e-7a2c-4f3b-9a8e-2c1d5f6a7b8c",
            "policy_no": "POL-000452",
            "risk_note_no": "RN-000452",
            "product_line": "motor",
            "vehicle_class": "private",
            "cover_type": "comprehensive",
            "status": "pending_payment",
            "start_on": "2026-10-14",
            "end_on": "2027-10-13",
            "payment_mode": "full",
            "instalments": 1,
            "sum_insured": {
                "amount": 850000,
                "amount_minor": 85000000
            },
            "basic_premium": {
                "amount": 30000,
                "amount_minor": 3000000
            },
            "taxes": {
                "amount": 175,
                "amount_minor": 17500
            },
            "gross_premium": {
                "amount": 30175,
                "amount_minor": 3017500
            },
            "activated_at": null,
            "cancelled_at": null,
            "cancel_reason": null
        },
        "client": {
            "client_no": "C000123",
            "name": "Wanjiru Kamau",
            "type": "individual",
            "phone": "0712345678",
            "email": "wanjiru@example.co.ke"
        },
        "insurer": {
            "id": 3,
            "name": "Savanna General"
        },
        "vehicle": null
    }
}
policy.activated Policy status pending_payment → active

Cover went live: the first debit note was settled (cash) or Flexysure Finance disbursed the loan.

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "policy.activated",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "policy": {
            "uuid": "0d7b1f1e-7a2c-4f3b-9a8e-2c1d5f6a7b8c",
            "policy_no": "POL-000452",
            "risk_note_no": "RN-000452",
            "product_line": "motor",
            "vehicle_class": "private",
            "cover_type": "comprehensive",
            "status": "active",
            "start_on": "2026-10-14",
            "end_on": "2027-10-13",
            "payment_mode": "full",
            "instalments": 1,
            "sum_insured": {
                "amount": 850000,
                "amount_minor": 85000000
            },
            "basic_premium": {
                "amount": 30000,
                "amount_minor": 3000000
            },
            "taxes": {
                "amount": 175,
                "amount_minor": 17500
            },
            "gross_premium": {
                "amount": 30175,
                "amount_minor": 3017500
            },
            "activated_at": "2026-10-14T09:31:04+03:00",
            "cancelled_at": null,
            "cancel_reason": null
        },
        "client": {
            "client_no": "C000123",
            "name": "Wanjiru Kamau",
            "type": "individual",
            "phone": "0712345678",
            "email": "wanjiru@example.co.ke"
        },
        "insurer": {
            "id": 3,
            "name": "Savanna General"
        },
        "vehicle": {
            "reg_no": "KDA 123A",
            "chassis_no": "NZE141-3012456",
            "make": "Toyota",
            "model": "Axio",
            "year": 2018,
            "value": {
                "amount": 850000,
                "amount_minor": 85000000
            }
        }
    }
}
policy.endorsed Endorsement status → issued

An endorsement was issued on a policy (premium delta positive, negative or zero).

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "policy.endorsed",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "endorsement": {
            "uuid": "2b9d8c7e-1f2a-4b3c-8d9e-0f1a2b3c4d5e",
            "number": "END/2026/000012",
            "type": "change_value",
            "status": "issued",
            "effective_on": "2026-11-01",
            "premium_delta": {
                "amount": 2500,
                "amount_minor": 250000
            },
            "taxes_delta": {
                "amount": 11,
                "amount_minor": 1100
            },
            "gross_delta": {
                "amount": 2511,
                "amount_minor": 251100
            },
            "changes": {
                "sum_insured_minor": {
                    "from": 85000000,
                    "to": 95000000
                }
            },
            "issued_at": "2026-10-20T11:02:00+03:00"
        },
        "policy": {
            "uuid": "0d7b1f1e-7a2c-4f3b-9a8e-2c1d5f6a7b8c",
            "policy_no": "POL-000452"
        }
    }
}
policy.cancelled Policy status → cancelled

A policy was cancelled (client request, non-payment of premium finance, or other).

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "policy.cancelled",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "policy": {
            "uuid": "0d7b1f1e-7a2c-4f3b-9a8e-2c1d5f6a7b8c",
            "policy_no": "POL-000452",
            "risk_note_no": "RN-000452",
            "product_line": "motor",
            "vehicle_class": "private",
            "cover_type": "comprehensive",
            "status": "cancelled",
            "start_on": "2026-10-14",
            "end_on": "2027-10-13",
            "payment_mode": "full",
            "instalments": 1,
            "sum_insured": {
                "amount": 850000,
                "amount_minor": 85000000
            },
            "basic_premium": {
                "amount": 30000,
                "amount_minor": 3000000
            },
            "taxes": {
                "amount": 175,
                "amount_minor": 17500
            },
            "gross_premium": {
                "amount": 30175,
                "amount_minor": 3017500
            },
            "activated_at": "2026-10-14T09:31:04+03:00",
            "cancelled_at": "2026-12-01T08:15:00+03:00",
            "cancel_reason": "Client request"
        },
        "client": {
            "client_no": "C000123",
            "name": "Wanjiru Kamau",
            "type": "individual",
            "phone": "0712345678",
            "email": "wanjiru@example.co.ke"
        },
        "insurer": {
            "id": 3,
            "name": "Savanna General"
        },
        "vehicle": {
            "reg_no": "KDA 123A",
            "chassis_no": "NZE141-3012456",
            "make": "Toyota",
            "model": "Axio",
            "year": 2018,
            "value": {
                "amount": 850000,
                "amount_minor": 85000000
            }
        }
    }
}
policy.renewal_due flexysure:webhooks-renewal-due (daily 06:30)

An active policy entered a renewal window. Fired by the daily job at 60, 30 and 7 days before end_on.

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "policy.renewal_due",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "policy": {
            "uuid": "0d7b1f1e-7a2c-4f3b-9a8e-2c1d5f6a7b8c",
            "policy_no": "POL-000452",
            "risk_note_no": "RN-000452",
            "product_line": "motor",
            "vehicle_class": "private",
            "cover_type": "comprehensive",
            "status": "active",
            "start_on": "2026-10-14",
            "end_on": "2027-10-13",
            "payment_mode": "full",
            "instalments": 1,
            "sum_insured": {
                "amount": 850000,
                "amount_minor": 85000000
            },
            "basic_premium": {
                "amount": 30000,
                "amount_minor": 3000000
            },
            "taxes": {
                "amount": 175,
                "amount_minor": 17500
            },
            "gross_premium": {
                "amount": 30175,
                "amount_minor": 3017500
            },
            "activated_at": "2026-10-14T09:31:04+03:00",
            "cancelled_at": null,
            "cancel_reason": null
        },
        "client": {
            "client_no": "C000123",
            "name": "Wanjiru Kamau",
            "type": "individual",
            "phone": "0712345678",
            "email": "wanjiru@example.co.ke"
        },
        "insurer": {
            "id": 3,
            "name": "Savanna General"
        },
        "vehicle": {
            "reg_no": "KDA 123A",
            "chassis_no": "NZE141-3012456",
            "make": "Toyota",
            "model": "Axio",
            "year": 2018,
            "value": {
                "amount": 850000,
                "amount_minor": 85000000
            }
        },
        "renewal": {
            "days_to_expiry": 30,
            "window": "30d",
            "as_of": "2027-09-13"
        }
    }
}
certificate.issued DmvicCertificate status → issued

DMVIC issued the motor certificate. verify_url opens the public roadside check.

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "certificate.issued",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "certificate": {
            "certificate_no": "C12345678",
            "class": "C",
            "cover_code": "100",
            "status": "issued",
            "period_from": "2026-10-14",
            "period_to": "2027-10-13",
            "issued_at": "2026-10-14T09:31:12+03:00",
            "cancelled_at": null,
            "verify_url": "https://app.flexysure.com/verify/C12345678"
        },
        "policy": {
            "uuid": "0d7b1f1e-7a2c-4f3b-9a8e-2c1d5f6a7b8c",
            "policy_no": "POL-000452",
            "risk_note_no": "RN-000452"
        },
        "vehicle": {
            "reg_no": "KDA 123A",
            "chassis_no": "NZE141-3012456",
            "make": "Toyota",
            "model": "Axio",
            "year": 2018,
            "value": {
                "amount": 850000,
                "amount_minor": 85000000
            }
        }
    }
}
certificate.cancelled DmvicCertificate status → cancelled

A motor certificate was cancelled at DMVIC (policy cancelled, vehicle removed, non-payment).

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "certificate.cancelled",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "certificate": {
            "certificate_no": "C12345678",
            "class": "C",
            "cover_code": "100",
            "status": "cancelled",
            "period_from": "2026-10-14",
            "period_to": "2027-10-13",
            "issued_at": "2026-10-14T09:31:12+03:00",
            "cancelled_at": "2026-12-01T08:15:30+03:00",
            "verify_url": "https://app.flexysure.com/verify/C12345678"
        },
        "policy": {
            "uuid": "0d7b1f1e-7a2c-4f3b-9a8e-2c1d5f6a7b8c",
            "policy_no": "POL-000452",
            "risk_note_no": "RN-000452"
        },
        "vehicle": {
            "reg_no": "KDA 123A",
            "chassis_no": "NZE141-3012456",
            "make": "Toyota",
            "model": "Axio",
            "year": 2018,
            "value": {
                "amount": 850000,
                "amount_minor": 85000000
            }
        }
    }
}
claim.registered Claim created

A claim was registered on a policy.

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "claim.registered",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "claim": {
            "uuid": "7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928",
            "claim_no": "CLM/2026/000007",
            "claim_type": "accidental_damage",
            "legal": false,
            "status": "registered",
            "incident_on": "2026-11-20",
            "reported_on": "2026-11-21",
            "risk_item": "KDA 123A",
            "insurer_ref": null,
            "estimate": {
                "amount": 120000,
                "amount_minor": 12000000
            },
            "reserve": {
                "amount": 0,
                "amount_minor": 0
            },
            "approved": {
                "amount": 0,
                "amount_minor": 0
            },
            "paid": {
                "amount": 0,
                "amount_minor": 0
            }
        },
        "policy": {
            "uuid": "0d7b1f1e-7a2c-4f3b-9a8e-2c1d5f6a7b8c",
            "policy_no": "POL-000452"
        }
    }
}
claim.updated Claim status changed

A claim changed status. previous_status tells you where it came from.

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "claim.updated",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "claim": {
            "uuid": "7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928",
            "claim_no": "CLM/2026/000007",
            "claim_type": "accidental_damage",
            "legal": false,
            "status": "approved",
            "incident_on": "2026-11-20",
            "reported_on": "2026-11-21",
            "risk_item": "KDA 123A",
            "insurer_ref": "SG/CLM/88213",
            "estimate": {
                "amount": 120000,
                "amount_minor": 12000000
            },
            "reserve": {
                "amount": 120000,
                "amount_minor": 12000000
            },
            "approved": {
                "amount": 104000,
                "amount_minor": 10400000
            },
            "paid": {
                "amount": 0,
                "amount_minor": 0
            }
        },
        "policy": {
            "uuid": "0d7b1f1e-7a2c-4f3b-9a8e-2c1d5f6a7b8c",
            "policy_no": "POL-000452"
        },
        "previous_status": "under_assessment"
    }
}
prospect.created Prospect created

A prospect was added to the CRM pipeline (web form, API or staff).

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "prospect.created",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "prospect": {
            "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
            "name": "Mwangi Hardware Ltd",
            "status": "new",
            "email": "info@mwangihardware.co.ke",
            "phone": "0722000111",
            "sector": "Retail",
            "rating": "warm",
            "follow_up_date": "2026-10-20",
            "expected_premium": {
                "amount": 180000,
                "amount_minor": 18000000
            },
            "expected_commission": {
                "amount": 18000,
                "amount_minor": 1800000
            }
        }
    }
}
ipf.application.decided IpfApplication status submitted → approved | declined

Flexysure Finance approved or declined a premium-finance application. decision is approved or declined.

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "ipf.application.decided",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "application": {
            "uuid": "4c3b2a19-8d7e-4f60-9a1b-2c3d4e5f6a7b",
            "status": "approved",
            "tenor_months": 6,
            "gross_premium": {
                "amount": 50000,
                "amount_minor": 5000000
            },
            "deposit": {
                "amount": 10000,
                "amount_minor": 1000000
            },
            "principal": {
                "amount": 40000,
                "amount_minor": 4000000
            },
            "interest": {
                "amount": 6000,
                "amount_minor": 600000
            },
            "fees": {
                "amount": 500,
                "amount_minor": 50000
            },
            "total_repayable": {
                "amount": 46500,
                "amount_minor": 4650000
            },
            "apr_bps": 5600,
            "decided_at": "2026-10-14T10:02:00+03:00",
            "decline_reason": null
        },
        "policy": {
            "uuid": "0d7b1f1e-7a2c-4f3b-9a8e-2c1d5f6a7b8c",
            "policy_no": "POL-000452"
        },
        "client": {
            "client_no": "C000123",
            "name": "Wanjiru Kamau",
            "type": "individual",
            "phone": "0712345678",
            "email": "wanjiru@example.co.ke"
        },
        "decision": "approved"
    }
}
ipf.loan.disbursed IpfLoan status pending_disbursement → active

Flexysure Finance paid the underwriter; the loan is active and the policy is live. Includes the repayment schedule.

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "ipf.loan.disbursed",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "loan": {
            "uuid": "5d4c3b2a-1908-4f7e-8d6c-5b4a39281706",
            "loan_ref": "IPF-000087",
            "status": "active",
            "tenor_months": 6,
            "principal": {
                "amount": 40000,
                "amount_minor": 4000000
            },
            "interest": {
                "amount": 6000,
                "amount_minor": 600000
            },
            "fees": {
                "amount": 500,
                "amount_minor": 50000
            },
            "total_repayable": {
                "amount": 46500,
                "amount_minor": 4650000
            },
            "repaid": {
                "amount": 0,
                "amount_minor": 0
            },
            "outstanding": {
                "amount": 46500,
                "amount_minor": 4650000
            },
            "first_due_on": "2026-11-14",
            "disbursed_at": "2026-10-14T15:40:00+03:00"
        },
        "policy": {
            "uuid": "0d7b1f1e-7a2c-4f3b-9a8e-2c1d5f6a7b8c",
            "policy_no": "POL-000452"
        },
        "client": {
            "client_no": "C000123",
            "name": "Wanjiru Kamau",
            "type": "individual",
            "phone": "0712345678",
            "email": "wanjiru@example.co.ke"
        },
        "schedule": [
            {
                "seq": 1,
                "due_on": "2026-11-14",
                "amount_due": {
                    "amount": 7750,
                    "amount_minor": 775000
                },
                "status": "due"
            },
            {
                "seq": 2,
                "due_on": "2026-12-14",
                "amount_due": {
                    "amount": 7750,
                    "amount_minor": 775000
                },
                "status": "due"
            }
        ]
    }
}
ipf.instalment.missed IpfLoan status active → in_arrears | cancellation_pending (flexysure:ipf-arrears)

A premium-finance instalment is overdue and the loan moved into arrears. overdue lists every unpaid, past-due instalment.

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "ipf.instalment.missed",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "loan": {
            "uuid": "5d4c3b2a-1908-4f7e-8d6c-5b4a39281706",
            "loan_ref": "IPF-000087",
            "status": "in_arrears",
            "tenor_months": 6,
            "principal": {
                "amount": 40000,
                "amount_minor": 4000000
            },
            "interest": {
                "amount": 6000,
                "amount_minor": 600000
            },
            "fees": {
                "amount": 500,
                "amount_minor": 50000
            },
            "total_repayable": {
                "amount": 46500,
                "amount_minor": 4650000
            },
            "repaid": {
                "amount": 0,
                "amount_minor": 0
            },
            "outstanding": {
                "amount": 46500,
                "amount_minor": 4650000
            },
            "first_due_on": "2026-11-14",
            "disbursed_at": "2026-10-14T15:40:00+03:00"
        },
        "policy": {
            "uuid": "0d7b1f1e-7a2c-4f3b-9a8e-2c1d5f6a7b8c",
            "policy_no": "POL-000452"
        },
        "client": {
            "client_no": "C000123",
            "name": "Wanjiru Kamau",
            "type": "individual",
            "phone": "0712345678",
            "email": "wanjiru@example.co.ke"
        },
        "overdue": [
            {
                "seq": 1,
                "due_on": "2026-11-14",
                "amount_due": {
                    "amount": 7750,
                    "amount_minor": 775000
                },
                "amount_paid": {
                    "amount": 0,
                    "amount_minor": 0
                },
                "outstanding": {
                    "amount": 7750,
                    "amount_minor": 775000
                }
            }
        ]
    }
}
message.sent Message status → sent | delivered

An SMS or email left the outbox (status sent or delivered).

Payload JSON
{
    "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
    "event": "message.sent",
    "created_at": "2026-10-14T09:31:05+03:00",
    "api_version": "v1",
    "tenant": {
        "code": "SIMBA",
        "name": "Simba Risk Brokers"
    },
    "data": {
        "message": {
            "uuid": "8f7e6d5c-4b3a-4291-8079-6f5e4d3c2b1a",
            "channel": "sms",
            "to": "254712345678",
            "subject": null,
            "template_key": "policy.issued",
            "transaction": "Policy issued",
            "status": "sent",
            "provider": "africastalking",
            "provider_ref": "ATXid_9f1",
            "units": 1,
            "sent_at": "2026-10-14T09:31:08+03:00"
        }
    }
}
ping Test event Settings → 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.

Payload JSON
{
    "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:

  1. 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.
  2. Reject the request if t is more than 300 seconds from your clock. This stops replays.
  3. Compute the HMAC over the raw bytes you received, before parsing JSON. Re-encoding the JSON changes the bytes and breaks the signature.
  4. 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.
HeaderMeaning
Flexysure-Signaturet=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-EventThe event name, e.g. payment.received.
Flexysure-DeliveryUUID of the event. Identical on every retry: use it to ignore duplicates.
Content-Typeapplication/json
User-AgentFlexysure-Webhooks/1.0
What arrives HTTP
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.

  1. Attempt 1 · immediately
  2. Attempt 2 · +1 minute
  3. Attempt 3 · +5 minutes
  4. Attempt 4 · +30 minutes
  5. Attempt 5 · +2 hours
  6. Attempt 6 · +12 hours
  7. Failed

Verification samples

Each sample applies the same rules as our own Dispatcher::verify.

PHP
receiver.php PHP
<?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.js JavaScript
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.py Python
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.

GET /api/v1/webhooks Flexysure only

List webhook endpoints

Every endpoint registered for the token's organisation, newest first. Secrets are never returned here; see secret_hint.

Scope
webhooks
Authentication
Bearer token
Rate limit
60 requests per minute per token (per-client override)
Idempotency
Safe to repeat (read only).

Headers

HeaderMeaning
AuthorizationBearer <access_token> (scope webhooks)

Parameters

NameInTypeRequiredDescription
limit query integer No Rows per page, 1–100 (default 25).
cursor query string No Opaque cursor from links.next.
page query integer No Insurance-Cloud style page number (alternative to cursor).
per_page query integer No Rows per page when using page.

Example

Request cURL
curl -X GET 'https://flexysure.intrepidprojects.co.ke/api/v1/webhooks' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Accept: application/json'
Response JSON
{
    "data": [
        {
            "id": "b1f1c0de-1234-4abc-9def-0123456789ab",
            "url": "https://erp.simba.co.ke/flexysure/webhooks",
            "description": "Core ERP",
            "events": [
                "payment.received",
                "policy.activated"
            ],
            "active": true,
            "secret_hint": "whsec_…k9Qz",
            "secret_rotated_at": null,
            "last_delivered_at": "2026-10-14T09:31:06+03:00",
            "last_failed_at": null,
            "created_at": "2026-10-01T08:00:00+03:00"
        }
    ],
    "meta": {
        "limit": 25,
        "has_more": false,
        "count": 1
    },
    "links": {
        "next": null
    }
}

Responses

StatusMeaning
200 OK
401 unauthenticated
403 insufficient_scope, or agent_key_cannot_manage_webhooks for a key acting as an agent

Errors are problem documents; the common codes (401, 403 insufficient_scope, 422, 429) apply to every endpoint.

POST /api/v1/webhooks Flexysure only

Create a webhook endpoint

Registers a receiver URL. The response carries the signing secret exactly once; store it. Pass events ["*"] to receive everything, now and in future.

Scope
webhooks
Authentication
Bearer token
Rate limit
60 requests per minute per token (per-client override)
Idempotency
Not keyed. Check before retrying.

Headers

HeaderMeaning
AuthorizationBearer <access_token> (scope webhooks)
Content-Typeapplication/json

Body fields

FieldTypeRequiredDescription
url string Yes Absolute http(s) URL, max 2048 characters.
description string No Label shown in Settings, max 160 characters.
events string[] Yes Event names from GET /webhooks/events, or ["*"].
active boolean No Default true.

Example

Request cURL
curl -X POST 'https://flexysure.intrepidprojects.co.ke/api/v1/webhooks' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://erp.simba.co.ke/flexysure/webhooks",
    "description": "Core ERP",
    "events": [
        "payment.received",
        "policy.activated"
    ]
}'
Request body (JSON)
Request body JSON
{
    "url": "https://erp.simba.co.ke/flexysure/webhooks",
    "description": "Core ERP",
    "events": [
        "payment.received",
        "policy.activated"
    ]
}
Response JSON
{
    "data": {
        "id": "b1f1c0de-1234-4abc-9def-0123456789ab",
        "url": "https://erp.simba.co.ke/flexysure/webhooks",
        "description": "Core ERP",
        "events": [
            "payment.received",
            "policy.activated"
        ],
        "active": true,
        "secret_hint": "whsec_…k9Qz",
        "secret_rotated_at": null,
        "last_delivered_at": "2026-10-14T09:31:06+03:00",
        "last_failed_at": null,
        "created_at": "2026-10-01T08:00:00+03:00",
        "secret": "whsec_3Qf8sZl2pV9xK1mN0bT7cR4dE6gH8jL5wY2aU7oP"
    }
}

Responses

StatusMeaning
201 Created
401 unauthenticated
403 insufficient_scope, or agent_key_cannot_manage_webhooks for a key acting as an agent
422 validation_failed

Errors are problem documents; the common codes (401, 403 insufficient_scope, 422, 429) apply to every endpoint.

GET /api/v1/webhooks/events Flexysure only

List the event catalogue

Every event name a webhook endpoint can subscribe to, with a one-line description.

Scope
webhooks
Authentication
Bearer token
Rate limit
60 requests per minute per token (per-client override)
Idempotency
Safe to repeat (read only).

Headers

HeaderMeaning
AuthorizationBearer <access_token> (scope webhooks)

Example

Request cURL
curl -X GET 'https://flexysure.intrepidprojects.co.ke/api/v1/webhooks/events' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Accept: application/json'
Response JSON
{
    "data": [
        {
            "event": "payment.received",
            "description": "A receipt was recorded (M-Pesa STK, C2B or manual)."
        }
    ]
}

Responses

StatusMeaning
200 OK
401 unauthenticated
403 insufficient_scope, or agent_key_cannot_manage_webhooks for a key acting as an agent

Errors are problem documents; the common codes (401, 403 insufficient_scope, 422, 429) apply to every endpoint.

GET /api/v1/webhooks/{id} Flexysure only

Get one webhook endpoint

By its UUID.

Scope
webhooks
Authentication
Bearer token
Rate limit
60 requests per minute per token (per-client override)
Idempotency
Safe to repeat (read only).

Headers

HeaderMeaning
AuthorizationBearer <access_token> (scope webhooks)

Parameters

NameInTypeRequiredDescription
id path uuid Yes Endpoint id.

Example

Request cURL
curl -X GET 'https://flexysure.intrepidprojects.co.ke/api/v1/webhooks/{id}' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Accept: application/json'
Response JSON
{
    "data": {
        "id": "b1f1c0de-1234-4abc-9def-0123456789ab",
        "url": "https://erp.simba.co.ke/flexysure/webhooks",
        "description": "Core ERP",
        "events": [
            "payment.received",
            "policy.activated"
        ],
        "active": true,
        "secret_hint": "whsec_…k9Qz",
        "secret_rotated_at": null,
        "last_delivered_at": "2026-10-14T09:31:06+03:00",
        "last_failed_at": null,
        "created_at": "2026-10-01T08:00:00+03:00"
    }
}

Responses

StatusMeaning
200 OK
401 unauthenticated
403 insufficient_scope, or agent_key_cannot_manage_webhooks for a key acting as an agent
404 not_found

Errors are problem documents; the common codes (401, 403 insufficient_scope, 422, 429) apply to every endpoint.

DELETE /api/v1/webhooks/{id} Flexysure only

Delete a webhook endpoint

Removes the endpoint and its delivery history. Pending deliveries are dropped.

Scope
webhooks
Authentication
Bearer token
Rate limit
60 requests per minute per token (per-client override)
Idempotency
Not keyed. Check before retrying.

Headers

HeaderMeaning
AuthorizationBearer <access_token> (scope webhooks)

Parameters

NameInTypeRequiredDescription
id path uuid Yes Endpoint id.

Example

Request cURL
curl -X DELETE 'https://flexysure.intrepidprojects.co.ke/api/v1/webhooks/{id}' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Accept: application/json'

Responses

StatusMeaning
204 No content
401 unauthenticated
403 insufficient_scope, or agent_key_cannot_manage_webhooks for a key acting as an agent
404 not_found

Errors are problem documents; the common codes (401, 403 insufficient_scope, 422, 429) apply to every endpoint.

GET /api/v1/webhooks/{id}/deliveries Flexysure only

List deliveries for an endpoint

Newest first, with the exact body that was signed and sent, the attempt count, the receiver's HTTP status and the next retry time.

Scope
webhooks
Authentication
Bearer token
Rate limit
60 requests per minute per token (per-client override)
Idempotency
Safe to repeat (read only).

Headers

HeaderMeaning
AuthorizationBearer <access_token> (scope webhooks)

Parameters

NameInTypeRequiredDescription
id path uuid Yes Endpoint id.
status query string No pending | retrying | delivered | failed
event query string No Filter by event name.
limit query integer No Rows per page, 1–100 (default 25).
cursor query string No Opaque cursor from links.next.
page query integer No Insurance-Cloud style page number (alternative to cursor).
per_page query integer No Rows per page when using page.

Example

Request cURL
curl -X GET 'https://flexysure.intrepidprojects.co.ke/api/v1/webhooks/{id}/deliveries' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Accept: application/json'
Response JSON
{
    "data": [
        {
            "id": 9821,
            "event": "payment.received",
            "event_id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
            "status": "delivered",
            "attempt": 1,
            "response_code": 200,
            "last_error": null,
            "signed_at": "2026-10-14T09:31:05+03:00",
            "delivered_at": "2026-10-14T09:31:06+03:00",
            "next_attempt_at": null,
            "created_at": "2026-10-14T09:31:05+03:00",
            "payload": {
                "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
                "event": "payment.received",
                "created_at": "2026-10-14T09:31:05+03:00",
                "api_version": "v1",
                "tenant": {
                    "code": "SIMBA",
                    "name": "Simba Risk Brokers"
                },
                "data": {
                    "receipt": {
                        "number": "RCT-000311"
                    }
                }
            }
        }
    ],
    "meta": {
        "limit": 25,
        "has_more": false,
        "count": 1
    },
    "links": {
        "next": null
    }
}

Responses

StatusMeaning
200 OK
401 unauthenticated
403 insufficient_scope, or agent_key_cannot_manage_webhooks for a key acting as an agent
404 not_found

Errors are problem documents; the common codes (401, 403 insufficient_scope, 422, 429) apply to every endpoint.

POST /api/v1/webhook-deliveries/{id}/retry Flexysure only

Retry a delivery now

Queues one more attempt of a delivery (any status), keeping the same Flexysure-Delivery id. Fails with 409 when the endpoint is disabled.

Scope
webhooks
Authentication
Bearer token
Rate limit
60 requests per minute per token (per-client override)
Idempotency
Not keyed. Check before retrying.

Headers

HeaderMeaning
AuthorizationBearer <access_token> (scope webhooks)

Parameters

NameInTypeRequiredDescription
id path integer Yes Delivery id from the deliveries list.

Example

Request cURL
curl -X POST 'https://flexysure.intrepidprojects.co.ke/api/v1/webhook-deliveries/{id}/retry' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Accept: application/json'
Response JSON
{
    "data": {
        "id": 9821,
        "event": "payment.received",
        "event_id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
        "status": "pending",
        "attempt": 1,
        "response_code": 200,
        "last_error": null,
        "signed_at": "2026-10-14T09:31:05+03:00",
        "delivered_at": "2026-10-14T09:31:06+03:00",
        "next_attempt_at": null,
        "created_at": "2026-10-14T09:31:05+03:00",
        "payload": {
            "id": "6f1c2b3a-4d5e-4f60-8a71-9b2c3d4e5f60",
            "event": "payment.received",
            "created_at": "2026-10-14T09:31:05+03:00",
            "api_version": "v1",
            "tenant": {
                "code": "SIMBA",
                "name": "Simba Risk Brokers"
            },
            "data": {
                "receipt": {
                    "number": "RCT-000311"
                }
            }
        }
    }
}

Responses

StatusMeaning
202 Accepted
401 unauthenticated
403 insufficient_scope, or agent_key_cannot_manage_webhooks for a key acting as an agent
404 not_found
409 endpoint_disabled

Errors are problem documents; the common codes (401, 403 insufficient_scope, 422, 429) apply to every endpoint.