Independence Day offer · ₹52/employee/moClaim offer
INDPayroll
Payroll Engine API

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

EventFires when
employee.upsertedEmployee created or updated
payroll.run.calculatedRun calculated
payroll.run.paidRun marked paid
payroll.run.reversedRun reversed
payslip.readyPayslip PDF rendered
fnf.settledSettlement paid
statutory.file.readyStatutory file generated
import.completedBulk import finished

Subscribing

bash
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

json
{
  "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.

javascript
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: false on GET /webhooks.
  • Answer 410 Gone to have a subscription deactivated immediately.
  • Deliveries can arrive more than once and out of order. Treat id as the deduplication key.