Developers

API Integration

Learn how to integrate our API into your systems.

14 min read

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.