Webhooks
If you own the UI, do not poll. Register an HTTPS endpoint at POST /webhooks and INDPayroll tells you when a run is calculated, a payslip is ready, a settlement is paid or a statutory file has been generated.
Events
| Event | Fires when |
|---|---|
| employee.upserted | Employee created or updated |
| payroll.run.calculated | Run calculated |
| payroll.run.paid | Run marked paid |
| payroll.run.reversed | Run reversed |
| payslip.ready | Payslip PDF rendered |
| fnf.settled | Settlement paid |
| statutory.file.ready | Statutory file generated |
| import.completed | Bulk import finished |
Subscribing
curl -X POST https://api.indpayroll.com/v1/webhooks \
-H "Authorization: Bearer $INDP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/indpayroll",
"events": ["payroll.run.calculated", "payroll.run.paid", "payslip.ready"],
"organization_ids": [42]
}'The secret is returned onceThe creation response carries
secret. Store it then - it is never shown again, and without it you cannot verify a delivery.The envelope
{
"id": "evt_01J9K8M2QF",
"type": "payroll.run.calculated",
"created_at": "2026-09-30T10:12:49Z",
"organization_id": 42,
"api_version": "2026-09-01",
"data": {
"payroll_run_id": "run_2026_09_0042",
"period_start": "2026-09-01",
"period_end": "2026-09-30",
"status": "generated"
}
}Verifying a delivery
Recompute the signature over the raw body - parse it only after the comparison passes, and compare in constant time.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, headers, secret) {
const timestamp = headers['x-indp-timestamp'];
const sent = String(headers['x-indp-signature']).replace(/^sha256=/, '');
// Anything older than five minutes is a replay, not a delivery.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = createHmac('sha256', secret)
.update(timestamp + '.' + rawBody)
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(sent, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}Delivery and retries
- Answer any 2xx within 10 seconds. Do the work afterwards - acknowledge first, process second.
- Anything else is retried with exponential backoff for 24 hours.
- A subscription failing for seven consecutive days is deactivated, and shows as
active: falseonGET /webhooks. - Answer
410 Goneto have a subscription deactivated immediately. - Deliveries can arrive more than once and out of order. Treat
idas the deduplication key.