Authentication
Every call except GET /health carries credentials. You can send the API key as a bearer token, or sign each request with HMAC-SHA256 when your security review asks for per-request signatures. Both reach the same engine with the same scopes.
Keys
A key pair is issued per environment from the INDPayroll partner console: a key id (indp_live_... or indp_test_...) and a secret shown once. Live and sandbox keys are separate and cannot reach each other's data.
Bearer tokens
The simplest form: send the key itself. Optionally exchange it once at POST /auth/token for a 60-minute access token and send that instead, so the key itself stays out of most requests.
curl https://api.indpayroll.com/v1/organizations \
-H "Authorization: Bearer $INDP_API_KEY" \
-H "INDP-Version: 2026-09-01"HMAC signing
Send three headers instead of an Authorization header. The signature is a lowercase hex HMAC-SHA256 of {timestamp}.{method}.{path}.{sha256(body)}, keyed with your key secret.
| Header | Value |
|---|---|
| X-INDP-Key | The key id - the public half of the pair. |
| X-INDP-Timestamp | Unix seconds at the moment of signing. |
| X-INDP-Signature | Hex HMAC-SHA256 of the canonical string below. |
import { createHash, createHmac } from 'node:crypto';
function sign({ method, path, body, keyId, keySecret }) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyHash = createHash('sha256').update(body ?? '').digest('hex');
const canonical = [timestamp, method.toUpperCase(), path, bodyHash].join('.');
const signature = createHmac('sha256', keySecret).update(canonical).digest('hex');
return {
'X-INDP-Key': keyId,
'X-INDP-Timestamp': timestamp,
'X-INDP-Signature': signature,
};
}path is the path and query string as sent, without the host - /v1/payroll-runs/preview. Requests signed more than 300 seconds ago are rejected with 401 signature_expired, so sign at the moment you send, not when you build the payload.
Scopes
Keys are issued with the narrowest set that does the job. A call outside them returns 403 missing_scope naming the scope it wanted.
| Scope | Grants |
|---|---|
| org:read | Organizations, departments, designations, holidays, shifts. |
| org:write | Changing statutory identifiers, the pay calendar and org master data. |
| employees:read | The employee master, salary structures and history. |
| employees:write | Creating and updating employees, structures, increments and exits. |
| payroll:read | Runs, payslips, disbursement summaries and reports. |
| payroll:write | Preview, calculate, recalculate, hold, lock, mark paid. |
| payroll:admin | Unlocking a locked run and reversing a paid one. |
| statutory:read | PF, ESI, PT, LWF and TDS files, and the filing dashboard. |
Sandbox
Point at https://sandbox-api.indpayroll.com/v1 with a indp_test_ key. Same engine, same statutory rules, synthetic organizations - and nothing is ever submitted to EPFO, ESIC or TRACES. Build and test everything there first.