Authentication
Every request carries a bearer token. Keys are scoped to one environment and one company set, so a compromised sandbox key cannot read production payroll.
Rotate keys from Settings → Developers. Old keys stay valid for 24 hours after rotation to give deploys time to pick up the new value.
curl https://api.payflo.dev/v1/companies \
-H "Authorization: Bearer $PAYFLO_API_KEY" Idempotency
Payroll requests move money. Send an Idempotency-Key header on every POST so a retry after a network timeout does not create a second payroll run.
Keys are remembered for 24 hours. Replaying a key returns the original response rather than performing the write again.
curl -X POST https://api.payflo.dev/v1/payrolls \
-H "Authorization: Bearer $PAYFLO_API_KEY" \
-H "Idempotency-Key: run-2025-01-15-acme" \
-d company_id=cmp_8f2a Webhooks
Payroll is asynchronous. Rather than polling, subscribe to webhooks and react when a run settles or a filing is accepted.
- payroll.submitted — the run was accepted and is calculating
- payroll.settled — funds have moved and stubs are available
- filing.accepted — a tax authority acknowledged the filing
- filing.rejected — action required, includes a reason code
Verifying webhook signatures
Each delivery is signed with your endpoint secret. Compare using a constant-time function, and reject anything older than five minutes to blunt replay attempts.
import crypto from 'node:crypto';
function verify(payload, header, secret) {
const [ts, sig] = header.split(',').map((p) => p.split('=')[1]);
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${payload}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
} Rate limits
The default limit is 100 requests per second per key, burstable to 300. Responses include X-RateLimit-Remaining. On a 429, back off using the Retry-After header rather than a fixed sleep.