Davis-Bacon WH-347 Certified Payroll Pre-Check API Reference
Beta. Recomputes one weekly certified payroll, in the fields of Form WH-347 (Rev. January 2025), against the wage determination rates sent with it. Product page and free test form.
Endpoint
| Paid | POST https://www.spreadrun.com/api/v1/wh347-payroll-precheck, API key required, $25.00 per completed report |
|---|---|
| Demo | POST https://www.spreadrun.com/api/demo/wh347-payroll-precheck, no key, 512 KB, 10 runs per day |
| Body | An .xlsx workbook, or a JSON object with CSV tables. Any Content-Type; the format is detected from the content. |
Request
Workbook. Sheets named Header (two columns: field and value), Payroll, Wage Determination, and Apprenticeship when any worker is a registered apprentice. Row 1 of each table sheet holds the column names. Start from the sample workbook; its rates are invented.
JSON. The same tables as CSV text, with the header fields as an object:
{
"header": {
"project_name": "...",
"project_no": "...",
"payroll_no": 6,
"contractor_name": "...",
"contractor_address": "...",
"project_location": "...",
"wage_determination_no": "...",
"week_ending": "2026-09-26"
},
"cwhssa": true,
"payrollCsv": "entry_no,last_name,first_name,...,net_pay\\n1,...",
"wageDeterminationCsv": "classification,base_rate,fringe_rate\\nElectrician,38.50,18.25\\n...",
"apprenticeshipCsv": "classification,level,wage_percent,fringe_percent,ratio,program_name\\nElectrician,2,60,,1:1,..."
}Up to 4.4 MB and 5,000 rows per table. Money cells may include a dollar sign and thousands separators. The wage determination rates are never looked up: the check uses exactly the rates you send, copied from the wage determination in the contract.
Header fields
project_name | Project name |
project_no | Project or prime contract number |
payroll_no | Certified payroll number: 1 for the first week, then 2, 3 and so on |
contractor_name | Contractor or subcontractor business name |
contractor_address | Business address |
project_location | Project address, or at least the county and state |
wage_determination_no | Wage determination number(s) and modification |
week_ending | Week ending date: YYYY-MM-DD, MM/DD/YYYY or an Excel date |
cwhssa | Optional. yes (default) if the contract is subject to the Contract Work Hours and Safety Standards Act, otherwise no. In JSON, a top-level true or false. |
final_payroll | Optional. yes to mark the "final certified payroll" box on the filled form. |
contractor_role | Optional. prime or subcontractor, to mark that box on the filled form. |
Columns
Payroll: one row per worker per classification, as on the WH-347. Every column except the optional ones must be present; leave a cell blank or 0 where nothing applies.
| Column | WH-347 | Meaning |
|---|---|---|
entry_no | 1A | Worker entry number. Repeat it on each row when a worker has more than one classification. |
last_name, first_name, middle_initial | 1B to 1D | Worker name (middle initial optional). |
worker_id | 1E | Identifying number, such as the last four digits of the SSN. Never a full SSN. |
worker_type | 2 | J (journeyworker) or RA (registered apprentice). |
apprentice_level | 2 | Level of progression, for RA rows. Matches a level in the apprenticeship table. |
classification | 3 | Labor classification, spelled as in the wage determination table (case and spacing are ignored). |
st_1 to st_7, ot_1 to ot_7 | 4 | Straight time and overtime hours for each day of the workweek, first day to last. |
total_hours | 5 | Total hours for the row. |
st_rate, ot_rate | 6A | Hourly rate paid for straight time and overtime, without cash in lieu of fringe benefits. |
fringe_credit_hourly | page 2 | Optional. Total hourly credit for fringe benefit plans. |
fringe_credit | 6B | Total fringe benefit credit for the row, in dollars. |
cash_in_lieu_hourly | Optional. Hourly cash paid in lieu of fringe benefits. | |
cash_in_lieu | 6C | Payment in lieu of fringe benefits for the row, in dollars. |
gross_project | 7A | Gross amount earned on this project for the row. |
gross_all_work | 7B | Gross for all work in the week. Once per worker: on one of the worker's rows, or the same on each. |
tax_withholding, fica, other_deductions, total_deductions | 8 | Deductions for all work. Once per worker, like 7B. |
net_pay | 9 | Net pay for all work. Once per worker. |
Wage determination: classification, base_rate, fringe_rate (blank means 0). One row per classification.
Apprenticeship: classification, level, wage_percent (of the journeyworker basic rate), fringe_percent (blank when the program does not specify fringe benefits, so the full rate applies), ratio (apprentices:journeyworkers, such as 1:3), and optionally program_name.
Filled WH-347 (PDF)
Add ?form=pdf to a paid call. When the report is a PASS, the response also has filledForm, outside report: available, filename, contentType (application/pdf), bytes, base64 and a note. It is DOL Form WH-347, January 2025 revision, as published, with your values drawn on it: page 1 for every 8 payroll rows (entry numbers continue across pages), then page 2 with the project details, up to three apprenticeship programs and up to eight workers' total hourly fringe credit, and an addendum page for any more. Fringe plan names, types and numbers, the OA or SAA boxes, the Statement of Compliance boxes, the certifying official and the signature, date, telephone and email are left blank for the contractor.
For WARN or FAIL, filledForm is {"available": false, "reason": "..."}. Demo calls never get a form. The form is included in the report price, is built in memory for the response and is not stored, logged or cached. It contains your payroll values, as it must; the report still never does.
Report
| Field | Meaning |
|---|---|
status | FAIL if any error, WARN if only warnings, otherwise PASS. A PASS does not mean the payroll complies with Davis-Bacon requirements. |
form, weekEnding, overtimeRule | The form version the fields follow, the week ending date read from the header, and cwhssa or not-applied. |
counts | rows, workers, apprentices, classifications, totalHours. |
findings | Up to 500, errors first. Each has severity, ruleId, path (such as /payroll/row[3]/st_rate, counting data rows from 1), message and, for most rules, source. Findings never repeat a name, number or amount from the input. |
findingCount, findingCounts, ruleCounts, findingsTruncated | Totals, including findings beyond the 500 listed. |
notChecked, sources, scope | What the report does not cover, the documents rules come from, and what the verdict means. |
input, inputSha256 | Format (json or xlsx), size, and the SHA-256 of the request body. |
Rule IDs
Money comparisons allow one cent for rounding.
| Rule | Severity | Source | Meaning |
|---|---|---|---|
WH-HEADER-REQUIRED | error | wh347 | A WH-347 header field is missing. |
WH-HEADER-FORMAT | error | wh347 | Week ending date is not a date, the payroll number is not a whole number from 1, or cwhssa is not yes or no. |
WH-COLUMN-MISSING | error | wh347 | A required column is missing from the payroll, wage determination or apprenticeship table. Checks that need it are skipped. |
WH-REQUIRED | error | wh347 | A required cell is blank (including 7B, 8 and 9 for a worker, and rates where hours are reported). |
WH-FORMAT | error | SpreadRun | A number, percentage or ratio cell cannot be read, or is negative. |
WH-WD-CLASSIFICATION | error | wh347 | A classification on the payroll (or apprenticeship table) is not in the wage determination table. |
WH-WD-DUPLICATE | error | SpreadRun | A classification is listed twice in the wage determination table. |
WH-RATE-BASE | error | cfr5.31 | Straight time rate below the basic hourly rate. |
WH-RATE-OT-BASE | error | cfr5.32 | Overtime rate below one and one-half times the basic hourly rate (the apprentice rate for apprentices). |
WH-RATE-OT-PAID | warning | cfr5.32 | Overtime rate below one and one-half times the straight time rate paid. |
WH-FRINGE-SHORT | error | cfr5.31 | Wages, 6B and 6C together are less than the basic rate plus the fringe rate owed for the row's hours. |
WH-FRINGE-CREDIT-MATH | error | wh347 | 6B does not equal total hours times the hourly fringe credit. |
WH-CASH-LIEU-MATH | error | wh347 | 6C does not equal total hours times the hourly cash in lieu. |
WH-OT-HOURS | error | cfr5.5b | More than 40 straight time hours in the week for one worker, when cwhssa is yes. |
WH-HOURS-DAY | error | wh347 | More than 24 hours on one day in one row. |
WH-HOURS-TOTAL | error | wh347 | Column 5 does not equal the daily hours in column 4. |
WH-GROSS-SHORT | error | wh347 | 7A is less than hours times the rates paid. |
WH-GROSS-MISMATCH | warning | wh347 | 7A equals neither hours times rates nor that plus 6C. |
WH-GROSS-ALL-WORK | error | wh347 | 7B is less than the worker's 7A total. |
WH-DEDUCTIONS-TOTAL | error | wh347 | Total deductions is not tax withholdings plus FICA plus other. |
WH-NET-PAY | error | wh347 | Net pay is not 7B minus total deductions. |
WH-ENTRY-CONSISTENCY | error | wh347 | Rows with one entry number have different identifying numbers, or different weekly amounts. |
WH-ID-FULL-SSN | error | cfr5.5 | An identifying number shaped like a full Social Security number. |
WH-WORKER-TYPE | error | wh347 | worker_type is not J or RA. |
WH-APPR-LEVEL | error | wh347 | An RA row without a level of progression. |
WH-APPR-PROGRAM | error | cfr5.5 | No apprenticeship wage schedule was supplied for the row's classification and level, so the journeyworker rate applies. |
WH-APPR-RATE | error | cfr5.5 | Apprentice straight time rate below the level's percentage of the journeyworker basic rate. |
WH-APPR-RATIO | warning | cfr5.5 | More apprentices than the program ratio allows for the journeyworkers in the same classification, on one or more days of this payroll. |
Source keys, as returned in every report:
wh347 | DOL Wage and Hour Division, Instructions for Completing Form WH-347 (Rev. January 2025) |
cfr5.5 | 29 CFR 5.5(a), Davis-Bacon contract clauses: wage payment, certified payrolls, apprentices |
cfr5.5b | 29 CFR 5.5(b)(1), Contract Work Hours and Safety Standards Act overtime (contracts over $100,000) |
cfr5.31 | 29 CFR 5.31, meeting wage determination obligations (basic rate and fringe benefits) |
cfr5.32 | 29 CFR 5.32, overtime payments (fringe benefits and cash in lieu excluded from the basic rate) |
- DOL Wage and Hour Division: Form WH-347 (Rev. January 2025) and its instructions
- 29 CFR part 5: Davis-Bacon contract clauses (5.5), fringe benefit crediting (5.31) and overtime (5.32)
- SAM.gov wage determinations, where the rates for your contract are published
Not checked
- Whether the wage determination rates you supplied are the right ones for the contract, project location and dates. SpreadRun checks against the rates you send; it does not look them up.
- Whether workers are classified correctly for the work they actually did.
- Hours worked on other projects for the same contractor, which count toward the 40-hour overtime threshold but are not on this payroll.
- Whether apprentices are individually registered, and the apprentice ratio across the contractor's whole workforce on the job site (only this payroll is counted).
- Whether fringe benefit plans are bona fide, annualization of fringe credits, and approval of unfunded plans.
- Whether deductions are permitted under 29 CFR part 3.
- The Statement of Compliance and signature on page 2.
Example responses
A paid check of the clean sample payroll (report shortened to its summary fields), and the first findings from a demo check of the sample with planted problems. Both generated by running the real endpoint code.
{
"requestId": "7f3c2a1e-5b8d-4c6f-9e0a-1d2b3c4d5e6f",
"api": "wh347-payroll-precheck",
"mode": "paid",
"charged": true,
"priceCents": 2500,
"balanceCents": 7500,
"report": {
"schemaVersion": 1,
"status": "PASS",
"form": "WH-347 (Rev. January 2025)",
"weekEnding": "2026-09-26",
"overtimeRule": "cwhssa",
"counts": {
"rows": 5,
"workers": 4,
"apprentices": 1,
"classifications": 4,
"totalHours": "164.00"
},
"findingCount": 0,
"findingCounts": {
"error": 0,
"warning": 0
},
"ruleCounts": {},
"findings": [],
"findingsTruncated": false,
"scope": "Recomputes the payroll against the wage determination rates supplied. A PASS does not mean the payroll complies with Davis-Bacon requirements, and this is not legal or compliance advice.",
"input": {
"format": "json",
"bytes": 1902
},
"inputSha256": "36a5a7ab053a3ab899ee436ccb0693dbcec207fe08057db699e76a0b5c880291"
}
}[
{
"severity": "error",
"ruleId": "WH-APPR-PROGRAM",
"path": "/payroll/row[8]/apprentice_level",
"message": "No apprenticeship program wage schedule was supplied for this classification and level, so the journeyworker rate on the wage determination applies.",
"source": "cfr5.5"
},
{
"severity": "error",
"ruleId": "WH-DEDUCTIONS-TOTAL",
"path": "/payroll/row[4]/total_deductions",
"message": "Total deductions does not equal tax withholdings plus FICA plus other deductions.",
"source": "wh347"
},
{
"severity": "error",
"ruleId": "WH-FRINGE-SHORT",
"path": "/payroll/row[3]",
"message": "Wages, fringe benefit credit (6B) and cash in lieu of fringe benefits (6C) together are less than the basic hourly rate plus the fringe benefit rate owed for these hours (overtime premium on the basic rate only).",
"source": "cfr5.31"
},
{
"severity": "error",
"ruleId": "WH-FRINGE-SHORT",
"path": "/payroll/row[6]",
"message": "Wages, fringe benefit credit (6B) and cash in lieu of fringe benefits (6C) together are less than the basic hourly rate plus the fringe benefit rate owed for these hours (overtime premium on the basic rate only).",
"source": "cfr5.31"
}
]Errors
See the shared error table. Rejected with HTTP 400 and not charged: an empty body, a PDF, a ZIP that is not an .xlsx workbook, a workbook without Header, Payroll and Wage Determination sheets, DOCTYPE or entity declarations, JSON without payrollCsv or wageDeterminationCsv, CSV with repeated column names or ragged rows, more than 5,000 rows. Example (HTTP 400):
{
"error": {
"code": "input_error",
"message": "wageDeterminationCsv is required: the table as CSV text, with a header row.",
"requestId": "7f3c2a1e-5b8d-4c6f-9e0a-1d2b3c4d5e6f",
"charged": false
}
}Code samples
# Excel workbook
curl -X POST "https://www.spreadrun.com/api/v1/wh347-payroll-precheck" \
-H "Authorization: Bearer $SPREADRUN_API_KEY" \
-H "Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" \
--data-binary @payroll_week_06.xlsx
# JSON with CSV tables
curl -X POST "https://www.spreadrun.com/api/v1/wh347-payroll-precheck" \
-H "Authorization: Bearer $SPREADRUN_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @payroll_week_06.json