Developers

Webhooks

Instead of asking Polisync what changed, let Polisync tell you. When a customer, policy, renewal or claim changes, we send a signed HTTPS request to your endpoint within seconds.

Set up an endpoint

  1. Build an HTTPS endpoint on a public address that accepts POST requests with a JSON body.
  2. In Polisync, open Settings, Developers, then Webhooks, Add endpoint. Enter the URL and choose the events you want, or all of them.
  3. Copy the signing secret (it starts with whsec_ and is shown once) into your server's secret store.
  4. Press Send test event to check your endpoint answers. It sends a ping event 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):

policy.created
{
  "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:

HeaderMeaning
Content-TypeAlways application/json.
User-AgentPolisync-Webhooks/1.0
Polisync-SignatureThe signature to verify, described below.
Polisync-Event-IdThe event id (evt_...). The same on every retry and on resends, so use it to ignore duplicates.
Polisync-Event-TypeThe event type, such as policy.created.
Polisync-Delivery-IdIdentifies this delivery to one endpoint.
Polisync-Delivery-Attempt1 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
  1. Split the header: t is the Unix time the request was signed, and each v1 is a signature.
  2. 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.
  3. Compute the HMAC-SHA256 of that text with your signing secret, as hex.
  4. Compare it with each v1 using a constant-time comparison. Accept the request if one matches.
  5. Reject the request if t is 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 2xx status 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).
customer.created payload
{
  "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.
customer.updated payload
{
  "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.

customer.deleted payload
{
  "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.
policy.created payload
{
  "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.
policy.updated payload
{
  "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.
policy.deleted payload
{
  "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.

policy.expiring payload
{
  "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.
renewal.recorded payload
{
  "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.

renewal.lapsed payload
{
  "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.
claim.created payload
{
  "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.

claim.status_changed payload
{
  "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.

claim.follow_up_due payload
{
  "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.

claim.deadline_overdue payload
{
  "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.deleted and policy.deleted tell 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.