Processing Payroll via the Friday API
This guide walks you through running payroll end-to-end using the Friday API. By the end you'll understand how to fetch available pay periods, build a payroll draft, review the cost preview, and approve the payroll for processing.
Before You Start
Plan requirement — All payroll endpoints require a payroll plan or higher on the API key's company. If the company is on a timetracking-only plan, requests will return 403.
Company setup — The company must be fully onboarded for payroll (bank account linked, tax details configured). If payroll hasn't been set up yet, you'll receive a 404 with a message indicating the company is not configured for payroll.
Base URL — All endpoints below are relative to /partner/v1.
The Happy Path
┌──────────────────────────────────────┐
│ 1. GET /payrolls/period-options │ ← What periods are available?
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 2. POST /payrolls/initialize │ ← Pre-populate employee earnings
└──────────────────┬───────────────────┘
▼
(review & edit data)
▼
┌──────────────────────────────────────┐
│ 3. POST /payrolls │ ← Create draft & get cost preview
└──────────────────┬───────────────────┘
▼
(review preview totals)
▼
┌──────────────────────────────────────┐
│ 4. POST /payrolls/:id/approve │ ← Submit for processing
└──────────────────────────────────────┘
Each step feeds into the next. The API is designed so you can pass response data from one step directly into the request for the next.
Step 1: Discover Available Pay Periods
GET /payrolls/period-options
This returns the pay periods that are ready to be run, grouped by pay schedule group (e.g., "Biweekly Salaried", "Weekly Hourly"). Each period option tells you:
| Field | What it means |
|---|---|
start / end | The earning period date range |
checkDate | The payday (when employees get paid) |
pay_frequency | weekly, biweekly, semimonthly, or monthly |
pay_schedule_group_id | Which group of employees this period covers |
employee_count | Total employees assigned to this group |
eligible_payroll_employee_count | Employees eligible to be paid in this period |
isDraft | true if a draft already exists for this period (you can resume it) |
actions.initialize | A ready-made request body — pass it directly to Step 2 |
Tip: The actions.initialize object is designed to be used as-is. You can grab it from the response and POST it straight to the initialize endpoint without modification.
Step 2: Initialize Payroll Data
POST /payrolls/initialize
This takes a period (from Step 1) and returns pre-populated earnings data for every eligible employee and contractor. Think of it as the "starting spreadsheet" — hours, rates, PTO, bonuses, and other compensation fields are filled in with defaults from the system.
Request body — use the actions.initialize object from Step 1, or build your own:
{
"type": "regular",
"period_start": "2026-03-01",
"period_end": "2026-03-14",
"payday": "2026-03-18",
"pay_frequency": "biweekly",
"pay_schedule_group_id": 3,
"employee_ids": [40, 159]
}The employee_ids field is optional. Omit it to initialize data for all eligible employees; include it to scope to specific people.
Understanding the Response
Each employee entry comes back with:
payrollData— the editable earnings: hours, rates, salary, PTO, bonuses, reimbursements, custom earnings, etc.time_totals— read-only time tracking totals (what the time clock computed)gross_pay— the server-calculated gross based onpayrollDatapayment_method— how the employee gets paid (direct_depositormanual)
Here's a simplified example:
{
"employee_id": 123,
"display_name": "Jane Smith",
"payment_method": "direct_deposit",
"gross_pay": 2537.50,
"payrollData": {
"regular_hours": 80,
"regular_payment_amount": 25.00,
"overtime_hours": 5,
"overtime_payment_amount": 37.50,
"salary_amount": 0,
"pto_hours": { "vacation": 8, "sick": 0, "holiday": 0, "approved": 8 },
"pto_payment_amount": 25.00,
"bonus_amount": 0,
"commission_amount": 0,
"reimbursements_amount": 0
},
"time_totals": {
"regular_hours": 80,
"overtime_hours": 5,
"pto_hours": 8,
"paid_time_hours": 93
}
}At this point, you can modify any values in payrollData — adjust hours, add a bonus, change reimbursement amounts, etc. The time_totals are informational only and don't need to be sent back.
What About Splits?
Some employees will have splits instead of a flat payrollData. This happens when an employee's pay rate changed mid-period (e.g., they got a raise on March 8th in a March 1–14 pay period).
A split employee looks like this:
{
"employee_id": 456,
"display_name": "Bob Johnson",
"payment_method": "direct_deposit",
"gross_pay": 3200.00,
"splits": [
{
"earning_period_start": "2026-03-01",
"earning_period_end": "2026-03-07",
"payrollData": { "regular_hours": 40, "regular_payment_amount": 20.00 }
},
{
"earning_period_start": "2026-03-08",
"earning_period_end": "2026-03-14",
"payrollData": { "regular_hours": 40, "regular_payment_amount": 22.00 }
}
]
}Key rule: An employee has either payrollData or splits, never both. When an employee has splits, edit each split's payrollData individually.
Step 3: Create the Draft
POST /payrolls
Send the (optionally modified) payroll data back to create a draft. The server calculates taxes, benefits, deductions, and net pay, then returns a full cost preview.
Request body:
{
"type": "regular",
"period_start": "2026-03-01",
"period_end": "2026-03-14",
"payday": "2026-03-18",
"pay_frequency": "biweekly",
"pay_schedule_group_id": 3,
"employees": [
{
"employee_id": 123,
"payment_method": "direct_deposit",
"note": "Extra shift bonus",
"payrollData": {
"regular_hours": 80,
"regular_payment_amount": 25.00,
"overtime_hours": 5,
"overtime_payment_amount": 37.50,
"bonus_amount": 200,
"salary_amount": 0,
"reimbursements_amount": 0
}
}
],
"contractors": [
{
"employee_id": 789,
"payment_method": "direct_deposit",
"payrollData": {
"salary_amount": 1500
}
}
]
}Validation rules to keep in mind:
type,period_start,period_end,paydayare always required.pay_frequencyandpay_schedule_group_idare required fortype: "regular".- Each employee must have
employee_idplus eitherpayrollDataorsplits(not both). - Contractors always use
payrollData(splits are not allowed for contractors). payment_methoddefaults todirect_deposit. It's automatically set tomanualfor employees without a bank account on file.
Reading the Preview Response
The response (HTTP 201) includes a full cost breakdown:
{
"data": {
"id": "pay_abc123def456",
"status": "draft",
"period_start": "2026-03-01",
"period_end": "2026-03-14",
"payday": "2026-03-18",
"type": "regular",
"totals": {
"employee_gross": "12500.00",
"employee_taxes": "2875.00",
"employee_net": "8975.00",
"contractor_gross": "3000.00",
"liability": "14137.50",
"cash_requirement": "14137.50"
},
"items": [
{
"id": "pit_abc123",
"employee": "emp_abc123",
"net_pay": "1850.00",
"earnings": [],
"taxes": [],
"benefits": [],
"post_tax_deductions": []
}
],
"contractor_payments": [
{
"id": "cpy_abc123",
"contractor": "cnt_abc123",
"amount": "1500.00",
"net_pay": "1500.00"
}
],
"actions": {
"approve": {
"method": "POST",
"path": "/partner/v1/payrolls/pay_abc123def456/approve"
}
}
}
}The key fields to review before approving:
totals.cash_requirement— total amount that will be debited from the company's bank accounttotals.employee_net— total take-home pay across all employeesitems— per-employee detail with earnings, taxes, benefits, and deductionscontractor_payments— per-contractor payment amounts
Step 4: Approve the Payroll
Once you're satisfied with the preview, approve the draft:
POST /payrolls/:payroll_id/approve
No request body is needed. On approval the system:
- Submits the payroll to the payment processor
- Assigns check numbers for any manual-payment employees
- Activates pending pay schedules
- Creates PTO accruals for hourly employees
- Sends confirmation emails and push notifications to company admins
The payroll transitions from draft → pending and will be processed on the payday date.
Optional Steps (Between Draft and Approve)
You don't have to go straight from draft to approval. The API supports a review-and-revise workflow:
Re-fetch the preview
GET /payrolls/:payroll_id/preview
Returns the same cost breakdown as the create-draft response. Useful if the user navigates away and comes back, or if you want to re-display the preview.
Update the draft
PATCH /payrolls/:payroll_id
Replaces the entire draft with a new payload. The request body follows the same shape as POST /payrolls. Use this when you need to change employee hours, add a bonus, or adjust any earnings after seeing the preview.
Delete the draft
DELETE /payrolls/:payroll_id
Discards the draft entirely. The period becomes available again for a new payroll run.
All three of these endpoints only work while the payroll is in draft status. Once approved, the draft is locked.
After Approval
Once a payroll is approved and processed, you can access additional data:
View Payroll Details
GET /payrolls/:payroll_id
Returns the full payroll record with updated status, per-employee items, and contractor payments.
List Past Payrolls
GET /payrolls
Paginated list of all payrolls for the company. Supports filters:
| Filter | Example | Description |
|---|---|---|
status | pending, paid, failed | Payroll processing status |
year | 2026 | Filter by tax year |
type | regular, off_cycle | Payroll type |
pay_frequency | biweekly | Pay schedule frequency |
Download Artifacts
After a payroll has been processed:
| Endpoint | What it returns |
|---|---|
GET /payrolls/:id/paystubs | Employee pay stubs (PDF download URL) |
GET /payrolls/:id/checks | Paper check images for manual-payment employees |
GET /payrolls/:id/checks/full | Detailed per-employee and per-contractor check data |
Run Payroll Reports
Seven report endpoints are available under /payroll-reports:
| Endpoint | Description |
|---|---|
GET /payroll-reports/company-payroll-summary | High-level payroll totals by period |
GET /payroll-reports/cash-requirement | Cash needed for upcoming payrolls |
GET /payroll-reports/tax-liability | Tax obligations breakdown |
GET /payroll-reports/tax-deposits | Tax deposit history |
GET /payroll-reports/w4-exemption-status | Employee W-4 filing statuses |
GET /payroll-reports/w2-preview | Year-end W-2 data preview |
GET /payroll-reports/contractor-payments | 1099-eligible contractor payment summary |
Staying in Sync with Webhooks
If you've registered a webhook endpoint, Friday will notify you of payroll lifecycle events automatically:
| Event | When it fires |
|---|---|
payroll.created | A payroll draft is created |
payroll.updated | A draft is modified |
payroll.deleted | A draft is discarded |
payroll.approved | A payroll is approved for processing |
payroll.paid | A payroll has been successfully paid |
payroll.failed | A payroll encountered a processing error |
See the Webhook Integration Guide in the developer portal for setup instructions, payload format, and signature verification.
Common Pitfalls
Sending both payrollData and splits for the same employee — The API enforces mutual exclusivity. If the initialize response gave you splits, send back splits. If it gave you payrollData, send back payrollData. Mixing them returns a validation error.
Forgetting pay_frequency or pay_schedule_group_id — These are required for regular type payrolls. They're included in the actions.initialize body from Step 1 automatically.
Trying to approve a non-draft payroll — Only payrolls with status: "draft" can be approved, updated, previewed, or deleted. Once approved, the payroll is locked.
Editing time_totals or gross_pay — These are read-only / server-computed fields. The server ignores them on create and update. Edit payrollData to change compensation values; gross_pay will be recalculated from those fields.
Contractors with splits — Contractors never have splits. Always use a flat payrollData for contractor entries.
Updated about 2 months ago
