Webhooks
Set up an endpoint
- Build an HTTPS endpoint on a public address that accepts
POSTrequests with a JSON body. - In Polisync, open Settings, Developers, then Webhooks, Add endpoint. Enter the URL and choose the events you want, or all of them.
- Copy the signing secret (it starts with
whsec_and is shown once) into your server's secret store. - Press Send test event to check your endpoint answers. It sends a
pingevent and shows the response.
You can have up to 5 endpoints. Each can listen to different events, so you can send claims to one system and customers to another.
Where the URL can point
The URL must start with https:// and be reachable from the internet. Addresses on private networks, localhost and cloud metadata services are refused, and redirects are not followed. To develop locally, expose your machine with a tunnel such as ngrok or Cloudflare Tunnel and use that HTTPS address.
What you receive
Every event is a POST with this body. data.object is the record that changed, in the same shape as the API returns it (with PAN always masked):
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "policy.created",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0003xyzpolicy",
"policyNumber": "HLT-2026-004512",
"insuranceType": "health",
"customerId": "cmv1a2b3c0001xyzcustomer",
"agencyId": "cmv1a2b3c0002xyzagency",
"insurerId": "cmr23hrx5000nxemeinsurer",
"insurerName": "Star Health and Allied Insurance Co. Ltd.",
"planName": "Family Health Optima",
"premium": 18500,
"sumInsured": 1000000,
"firstYearCommission": 2775,
"startDate": "2026-11-01",
"expiryDate": "2027-10-31",
"paymentMode": "annual",
"status": "active",
"notes": null,
"typeFields": {},
"nominees": [
{
"id": "cmv1a2b3c0004xyznominee",
"name": "Sunita Patel",
"dateOfBirth": "1988-07-02",
"relation": "spouse",
"sharePercent": 100,
"phone": "+919876500001"
}
],
"createdAt": "2026-10-26T09:41:00.000Z",
"updatedAt": "2026-10-26T09:41:00.000Z"
}
}
}These headers come with it:
| Header | Meaning |
|---|---|
Content-Type | Always application/json. |
User-Agent | Polisync-Webhooks/1.0 |
Polisync-Signature | The signature to verify, described below. |
Polisync-Event-Id | The event id (evt_...). The same on every retry and on resends, so use it to ignore duplicates. |
Polisync-Event-Type | The event type, such as policy.created. |
Polisync-Delivery-Id | Identifies this delivery to one endpoint. |
Polisync-Delivery-Attempt | 1 for the first try, up to 7. |
Because the same event can arrive more than once (a retry after a slow answer, or a resend from the delivery log), use the event id to ignore duplicates. Events can also arrive slightly out of order. When order matters, compare data.object.updatedAt with what you already have, or fetch the record with the API.
Verify the signature
Anyone who knows your URL can send it a request, so check every one. Polisync signs the exact body with your secret using HMAC-SHA256 and puts the result in the Polisync-Signature header:
Polisync-Signature: t=1793000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e9c8d8a7b2f6e4c1d9a03- Split the header:
tis the Unix time the request was signed, and eachv1is a signature. - Build the signed text: the timestamp, a dot, then the raw request body, exactly as received. Do not parse and re-serialise the JSON first, because that can change the bytes.
- Compute the HMAC-SHA256 of that text with your signing secret, as hex.
- Compare it with each
v1using a constant-time comparison. Accept the request if one matches. - Reject the request if
tis more than 5 minutes from your clock. That stops someone replaying a captured request.
const crypto = require("node:crypto")
// rawBody: the request body exactly as received (a Buffer or string), not re-serialised JSON.
function verify(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = signatureHeader.split(",").map((p) => p.split("="))
const timestamp = parts.find(([k]) => k === "t")?.[1]
const signatures = parts.filter(([k]) => k === "v1").map(([, v]) => v)
if (!timestamp || signatures.length === 0) return false
// Reject old requests so a captured one cannot be replayed.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody.toString("utf8")}`)
.digest("hex")
return signatures.some(
(s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected))
)
}
Using it in a server:
const express = require("express")
const app = express()
// The signature covers the raw bytes, so read the body raw for this route.
app.post("/webhooks/polisync", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body, req.get("Polisync-Signature") ?? "", process.env.POLISYNC_WEBHOOK_SECRET)) {
return res.status(400).send("Invalid signature")
}
const event = JSON.parse(req.body.toString("utf8"))
// Answer fast, then do the work. Polisync waits 5 seconds.
res.sendStatus(200)
handle(event) // ignore the event if you have already processed event.id
})
Rotating a secret
Choose Rotate secret on an endpoint to get a new one. For the next 24 hours every request carries two v1 signatures, one for the old secret and one for the new, and the code above accepts either. Deploy the new secret and nothing is missed.
Delivery and retries
- Answer with any
2xxstatus within 5 seconds to acknowledge. Do the real work after answering. - Anything else (an error status, a timeout, a redirect, no connection) counts as a failure and is retried, up to 7 attempts in all. The waits between attempts are: 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 24 hours.
- The delivery log in Polisync shows every event, the answer your endpoint gave, how long it took, and when the next retry is. Use Resend to send one again.
- If an endpoint keeps failing for 3 days in a row, Polisync switches it off and tells the workspace owner in the app and by email. Fix it, then resume it from Developers.
- Delivery logs are kept for 30 days.
Events
Open an event to see its payload. Events are only sent for changes in your own workspace. More event types may be added over time, so ignore types you do not handle.
Customers
customer.createdA customer was added (in the app, by import, or through the API).
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "customer.created",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0001xyzcustomer",
"name": "Ramesh Patel",
"type": "individual",
"phone": "+919876543210",
"altPhone": null,
"email": "ramesh.patel@gmail.com",
"dateOfBirth": "1985-03-14",
"gender": "male",
"address": "12 Shanti Nagar",
"city": "Surat",
"state": "Gujarat",
"pin": "395007",
"country": "IN",
"pan": "ABCDE****F",
"agencyId": "cmv1a2b3c0002xyzagency",
"status": "active",
"consentGiven": true,
"consentDate": "2026-10-26T09:30:00.000Z",
"createdAt": "2026-10-26T09:30:00.000Z",
"updatedAt": "2026-10-26T09:30:00.000Z"
}
}
}customer.updatedA customer's details were changed.
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "customer.updated",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0001xyzcustomer",
"name": "Ramesh Patel",
"type": "individual",
"phone": "+919876543210",
"altPhone": null,
"email": "ramesh.patel@gmail.com",
"dateOfBirth": "1985-03-14",
"gender": "male",
"address": "12 Shanti Nagar",
"city": "Surat",
"state": "Gujarat",
"pin": "395007",
"country": "IN",
"pan": "ABCDE****F",
"agencyId": "cmv1a2b3c0002xyzagency",
"status": "active",
"consentGiven": true,
"consentDate": "2026-10-26T09:30:00.000Z",
"createdAt": "2026-10-26T09:30:00.000Z",
"updatedAt": "2026-10-26T09:30:00.000Z"
}
}
}customer.deletedA customer, with their policies and claims, was deleted.
The customer's policies and claims are deleted with them. deletedPolicyIds lists the policies, so you can remove them too.
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "customer.deleted",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0001xyzcustomer",
"deleted": true
},
"deletedPolicyIds": [
"cmv1a2b3c0003xyzpolicy"
]
}
}Policies
policy.createdA policy was added.
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "policy.created",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0003xyzpolicy",
"policyNumber": "HLT-2026-004512",
"insuranceType": "health",
"customerId": "cmv1a2b3c0001xyzcustomer",
"agencyId": "cmv1a2b3c0002xyzagency",
"insurerId": "cmr23hrx5000nxemeinsurer",
"insurerName": "Star Health and Allied Insurance Co. Ltd.",
"planName": "Family Health Optima",
"premium": 18500,
"sumInsured": 1000000,
"firstYearCommission": 2775,
"startDate": "2026-11-01",
"expiryDate": "2027-10-31",
"paymentMode": "annual",
"status": "active",
"notes": null,
"typeFields": {},
"nominees": [
{
"id": "cmv1a2b3c0004xyznominee",
"name": "Sunita Patel",
"dateOfBirth": "1988-07-02",
"relation": "spouse",
"sharePercent": 100,
"phone": "+919876500001"
}
],
"createdAt": "2026-10-26T09:41:00.000Z",
"updatedAt": "2026-10-26T09:41:00.000Z"
}
}
}policy.updatedA policy was changed, including status and expiry date.
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "policy.updated",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0003xyzpolicy",
"policyNumber": "HLT-2026-004512",
"insuranceType": "health",
"customerId": "cmv1a2b3c0001xyzcustomer",
"agencyId": "cmv1a2b3c0002xyzagency",
"insurerId": "cmr23hrx5000nxemeinsurer",
"insurerName": "Star Health and Allied Insurance Co. Ltd.",
"planName": "Family Health Optima",
"premium": 18500,
"sumInsured": 1000000,
"firstYearCommission": 2775,
"startDate": "2026-11-01",
"expiryDate": "2027-10-31",
"paymentMode": "annual",
"status": "active",
"notes": null,
"typeFields": {},
"nominees": [
{
"id": "cmv1a2b3c0004xyznominee",
"name": "Sunita Patel",
"dateOfBirth": "1988-07-02",
"relation": "spouse",
"sharePercent": 100,
"phone": "+919876500001"
}
],
"createdAt": "2026-10-26T09:41:00.000Z",
"updatedAt": "2026-10-26T09:41:00.000Z"
}
}
}policy.deletedA policy was deleted.
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "policy.deleted",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0003xyzpolicy",
"deleted": true
}
}
}policy.expiringA policy is 30, 7 or 0 days from expiry.
Sent once for each policy at 30, 7 and 0 days before expiry. daysUntilExpiry says which.
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "policy.expiring",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0003xyzpolicy",
"policyNumber": "HLT-2026-004512",
"insuranceType": "health",
"customerId": "cmv1a2b3c0001xyzcustomer",
"agencyId": "cmv1a2b3c0002xyzagency",
"insurerId": "cmr23hrx5000nxemeinsurer",
"insurerName": "Star Health and Allied Insurance Co. Ltd.",
"planName": "Family Health Optima",
"premium": 18500,
"sumInsured": 1000000,
"firstYearCommission": 2775,
"startDate": "2026-11-01",
"expiryDate": "2027-10-31",
"paymentMode": "annual",
"status": "active",
"notes": null,
"typeFields": {},
"nominees": [
{
"id": "cmv1a2b3c0004xyznominee",
"name": "Sunita Patel",
"dateOfBirth": "1988-07-02",
"relation": "spouse",
"sharePercent": 100,
"phone": "+919876500001"
}
],
"createdAt": "2026-10-26T09:41:00.000Z",
"updatedAt": "2026-10-26T09:41:00.000Z"
},
"daysUntilExpiry": 7
}
}Renewals
renewal.recordedA renewal or premium payment was recorded.
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "renewal.recorded",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0005xyzrenewal",
"policyId": "cmv1a2b3c0003xyzpolicy",
"policyNumber": "HLT-2026-004512",
"customerId": "cmv1a2b3c0001xyzcustomer",
"insuranceType": "health",
"expiryDate": "2027-10-31",
"daysLeft": 370,
"status": "active",
"stage": "upcoming",
"mode": "reissue",
"graceDaysLeft": 30,
"renewedAt": null,
"renewedPolicyId": null,
"commissionEarned": null,
"createdAt": "2026-10-26T09:41:00.000Z",
"updatedAt": "2026-10-26T09:41:00.000Z"
}
}
}renewal.lapsedA renewal lapsed because the grace period ended.
Sent when the grace period ends without a renewal being recorded.
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "renewal.lapsed",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0005xyzrenewal",
"policyId": "cmv1a2b3c0003xyzpolicy",
"policyNumber": "HLT-2026-004512",
"customerId": "cmv1a2b3c0001xyzcustomer",
"insuranceType": "health",
"expiryDate": "2027-10-31",
"daysLeft": 370,
"status": "active",
"stage": "upcoming",
"mode": "reissue",
"graceDaysLeft": 30,
"renewedAt": null,
"renewedPolicyId": null,
"commissionEarned": null,
"createdAt": "2026-10-26T09:41:00.000Z",
"updatedAt": "2026-10-26T09:41:00.000Z"
}
}
}Claims
claim.createdA claim was reported.
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "claim.created",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0006xyzclaim",
"policyId": "cmv1a2b3c0003xyzpolicy",
"customerId": "cmv1a2b3c0001xyzcustomer",
"agencyId": "cmv1a2b3c0002xyzagency",
"claimType": "health_reimbursement",
"claimTypeLabel": "Health: reimbursement",
"status": "submitted",
"claimNumber": "CLM-884213",
"incidentDate": "2026-10-20",
"intimatedAt": "2026-10-21",
"submittedAt": "2026-10-22",
"surveyReportAt": null,
"underInvestigation": false,
"amountClaimed": 85000,
"amountApproved": null,
"amountSettled": null,
"settledAt": null,
"rejectionReason": null,
"providerName": "City Hospital",
"handlerName": null,
"handlerContact": null,
"description": "Hospitalised for 3 days",
"nextFollowUpAt": "2026-10-30",
"closedAt": null,
"deadlines": [
{
"key": "health_reimbursement_settle",
"label": "Insurer to settle",
"rule": "Within 15 days of the claim being submitted",
"startsOn": "2026-10-22",
"dueOn": "2026-11-06",
"daysLeft": 11,
"state": "running",
"interestDue": false
}
],
"createdAt": "2026-10-21T08:15:00.000Z",
"updatedAt": "2026-10-22T10:02:00.000Z"
}
}
}claim.status_changedA claim moved to a new status.
previousStatus is the status before the change; object.status is the new one.
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "claim.status_changed",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0006xyzclaim",
"policyId": "cmv1a2b3c0003xyzpolicy",
"customerId": "cmv1a2b3c0001xyzcustomer",
"agencyId": "cmv1a2b3c0002xyzagency",
"claimType": "health_reimbursement",
"claimTypeLabel": "Health: reimbursement",
"status": "submitted",
"claimNumber": "CLM-884213",
"incidentDate": "2026-10-20",
"intimatedAt": "2026-10-21",
"submittedAt": "2026-10-22",
"surveyReportAt": null,
"underInvestigation": false,
"amountClaimed": 85000,
"amountApproved": null,
"amountSettled": null,
"settledAt": null,
"rejectionReason": null,
"providerName": "City Hospital",
"handlerName": null,
"handlerContact": null,
"description": "Hospitalised for 3 days",
"nextFollowUpAt": "2026-10-30",
"closedAt": null,
"deadlines": [
{
"key": "health_reimbursement_settle",
"label": "Insurer to settle",
"rule": "Within 15 days of the claim being submitted",
"startsOn": "2026-10-22",
"dueOn": "2026-11-06",
"daysLeft": 11,
"state": "running",
"interestDue": false
}
],
"createdAt": "2026-10-21T08:15:00.000Z",
"updatedAt": "2026-10-22T10:02:00.000Z"
},
"previousStatus": "reported"
}
}claim.follow_up_dueA claim's follow-up date has arrived.
Sent on the day a claim's next follow-up date arrives.
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "claim.follow_up_due",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0006xyzclaim",
"policyId": "cmv1a2b3c0003xyzpolicy",
"customerId": "cmv1a2b3c0001xyzcustomer",
"agencyId": "cmv1a2b3c0002xyzagency",
"claimType": "health_reimbursement",
"claimTypeLabel": "Health: reimbursement",
"status": "submitted",
"claimNumber": "CLM-884213",
"incidentDate": "2026-10-20",
"intimatedAt": "2026-10-21",
"submittedAt": "2026-10-22",
"surveyReportAt": null,
"underInvestigation": false,
"amountClaimed": 85000,
"amountApproved": null,
"amountSettled": null,
"settledAt": null,
"rejectionReason": null,
"providerName": "City Hospital",
"handlerName": null,
"handlerContact": null,
"description": "Hospitalised for 3 days",
"nextFollowUpAt": "2026-10-30",
"closedAt": null,
"deadlines": [
{
"key": "health_reimbursement_settle",
"label": "Insurer to settle",
"rule": "Within 15 days of the claim being submitted",
"startsOn": "2026-10-22",
"dueOn": "2026-11-06",
"daysLeft": 11,
"state": "running",
"interestDue": false
}
],
"createdAt": "2026-10-21T08:15:00.000Z",
"updatedAt": "2026-10-22T10:02:00.000Z"
}
}
}claim.deadline_overdueThe insurer passed an IRDAI deadline on a claim.
Sent once per deadline. deadline is the key of the clock that was missed.
{
"id": "evt_5f1c2a9e7b3d4c6a8e0f1b2c3d4e5f60",
"type": "claim.deadline_overdue",
"createdAt": "2026-10-26T09:41:00.000Z",
"data": {
"object": {
"id": "cmv1a2b3c0006xyzclaim",
"policyId": "cmv1a2b3c0003xyzpolicy",
"customerId": "cmv1a2b3c0001xyzcustomer",
"agencyId": "cmv1a2b3c0002xyzagency",
"claimType": "health_reimbursement",
"claimTypeLabel": "Health: reimbursement",
"status": "submitted",
"claimNumber": "CLM-884213",
"incidentDate": "2026-10-20",
"intimatedAt": "2026-10-21",
"submittedAt": "2026-10-22",
"surveyReportAt": null,
"underInvestigation": false,
"amountClaimed": 85000,
"amountApproved": null,
"amountSettled": null,
"settledAt": null,
"rejectionReason": null,
"providerName": "City Hospital",
"handlerName": null,
"handlerContact": null,
"description": "Hospitalised for 3 days",
"nextFollowUpAt": "2026-10-30",
"closedAt": null,
"deadlines": [
{
"key": "health_reimbursement_settle",
"label": "Insurer to settle",
"rule": "Within 15 days of the claim being submitted",
"startsOn": "2026-10-22",
"dueOn": "2026-11-06",
"daysLeft": 11,
"state": "running",
"interestDue": false
}
],
"createdAt": "2026-10-21T08:15:00.000Z",
"updatedAt": "2026-10-22T10:02:00.000Z"
},
"deadline": "health_reimbursement_settle"
}
}ping is sent only by the Send test event button.
Best practices
- Verify every request and reject the ones that fail, with a 400.
- Answer first, process later. Put the event on a queue and return 200 quickly so you are not retried while still working.
- Be idempotent. Keep the ids of events you have handled and skip repeats.
- Treat the payload as a notification. For anything critical, fetch the current record with the API using the id in the event.
- Handle deletes.
customer.deletedandpolicy.deletedtell you to remove your copy. - Protect the data. Payloads hold personal and sometimes health information. Keep the endpoint on HTTPS, do not log whole payloads, and store only what you need.
- Watch your delivery log after you go live, and rotate the secret if you think it leaked.