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

PaidPOST https://www.spreadrun.com/api/v1/wh347-payroll-precheck, API key required, $25.00 per completed report
DemoPOST https://www.spreadrun.com/api/demo/wh347-payroll-precheck, no key, 512 KB, 10 runs per day
BodyAn .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_nameProject name
project_noProject or prime contract number
payroll_noCertified payroll number: 1 for the first week, then 2, 3 and so on
contractor_nameContractor or subcontractor business name
contractor_addressBusiness address
project_locationProject address, or at least the county and state
wage_determination_noWage determination number(s) and modification
week_endingWeek ending date: YYYY-MM-DD, MM/DD/YYYY or an Excel date
cwhssaOptional. 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_payrollOptional. yes to mark the "final certified payroll" box on the filled form.
contractor_roleOptional. 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.

ColumnWH-347Meaning
entry_no1AWorker entry number. Repeat it on each row when a worker has more than one classification.
last_name, first_name, middle_initial1B to 1DWorker name (middle initial optional).
worker_id1EIdentifying number, such as the last four digits of the SSN. Never a full SSN.
worker_type2J (journeyworker) or RA (registered apprentice).
apprentice_level2Level of progression, for RA rows. Matches a level in the apprenticeship table.
classification3Labor classification, spelled as in the wage determination table (case and spacing are ignored).
st_1 to st_7, ot_1 to ot_74Straight time and overtime hours for each day of the workweek, first day to last.
total_hours5Total hours for the row.
st_rate, ot_rate6AHourly rate paid for straight time and overtime, without cash in lieu of fringe benefits.
fringe_credit_hourlypage 2Optional. Total hourly credit for fringe benefit plans.
fringe_credit6BTotal fringe benefit credit for the row, in dollars.
cash_in_lieu_hourlyOptional. Hourly cash paid in lieu of fringe benefits.
cash_in_lieu6CPayment in lieu of fringe benefits for the row, in dollars.
gross_project7AGross amount earned on this project for the row.
gross_all_work7BGross 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_deductions8Deductions for all work. Once per worker, like 7B.
net_pay9Net 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

FieldMeaning
statusFAIL if any error, WARN if only warnings, otherwise PASS. A PASS does not mean the payroll complies with Davis-Bacon requirements.
form, weekEnding, overtimeRuleThe form version the fields follow, the week ending date read from the header, and cwhssa or not-applied.
countsrows, workers, apprentices, classifications, totalHours.
findingsUp 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, findingsTruncatedTotals, including findings beyond the 500 listed.
notChecked, sources, scopeWhat the report does not cover, the documents rules come from, and what the verdict means.
input, inputSha256Format (json or xlsx), size, and the SHA-256 of the request body.

Rule IDs

Money comparisons allow one cent for rounding.

RuleSeveritySourceMeaning
WH-HEADER-REQUIREDerrorwh347A WH-347 header field is missing.
WH-HEADER-FORMATerrorwh347Week 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-MISSINGerrorwh347A required column is missing from the payroll, wage determination or apprenticeship table. Checks that need it are skipped.
WH-REQUIREDerrorwh347A required cell is blank (including 7B, 8 and 9 for a worker, and rates where hours are reported).
WH-FORMATerrorSpreadRunA number, percentage or ratio cell cannot be read, or is negative.
WH-WD-CLASSIFICATIONerrorwh347A classification on the payroll (or apprenticeship table) is not in the wage determination table.
WH-WD-DUPLICATEerrorSpreadRunA classification is listed twice in the wage determination table.
WH-RATE-BASEerrorcfr5.31Straight time rate below the basic hourly rate.
WH-RATE-OT-BASEerrorcfr5.32Overtime rate below one and one-half times the basic hourly rate (the apprentice rate for apprentices).
WH-RATE-OT-PAIDwarningcfr5.32Overtime rate below one and one-half times the straight time rate paid.
WH-FRINGE-SHORTerrorcfr5.31Wages, 6B and 6C together are less than the basic rate plus the fringe rate owed for the row's hours.
WH-FRINGE-CREDIT-MATHerrorwh3476B does not equal total hours times the hourly fringe credit.
WH-CASH-LIEU-MATHerrorwh3476C does not equal total hours times the hourly cash in lieu.
WH-OT-HOURSerrorcfr5.5bMore than 40 straight time hours in the week for one worker, when cwhssa is yes.
WH-HOURS-DAYerrorwh347More than 24 hours on one day in one row.
WH-HOURS-TOTALerrorwh347Column 5 does not equal the daily hours in column 4.
WH-GROSS-SHORTerrorwh3477A is less than hours times the rates paid.
WH-GROSS-MISMATCHwarningwh3477A equals neither hours times rates nor that plus 6C.
WH-GROSS-ALL-WORKerrorwh3477B is less than the worker's 7A total.
WH-DEDUCTIONS-TOTALerrorwh347Total deductions is not tax withholdings plus FICA plus other.
WH-NET-PAYerrorwh347Net pay is not 7B minus total deductions.
WH-ENTRY-CONSISTENCYerrorwh347Rows with one entry number have different identifying numbers, or different weekly amounts.
WH-ID-FULL-SSNerrorcfr5.5An identifying number shaped like a full Social Security number.
WH-WORKER-TYPEerrorwh347worker_type is not J or RA.
WH-APPR-LEVELerrorwh347An RA row without a level of progression.
WH-APPR-PROGRAMerrorcfr5.5No apprenticeship wage schedule was supplied for the row's classification and level, so the journeyworker rate applies.
WH-APPR-RATEerrorcfr5.5Apprentice straight time rate below the level's percentage of the journeyworker basic rate.
WH-APPR-RATIOwarningcfr5.5More 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:

wh347DOL Wage and Hour Division, Instructions for Completing Form WH-347 (Rev. January 2025)
cfr5.529 CFR 5.5(a), Davis-Bacon contract clauses: wage payment, certified payrolls, apprentices
cfr5.5b29 CFR 5.5(b)(1), Contract Work Hours and Safety Standards Act overtime (contracts over $100,000)
cfr5.3129 CFR 5.31, meeting wage determination obligations (basic rate and fringe benefits)
cfr5.3229 CFR 5.32, overtime payments (fringe benefits and cash in lieu excluded from the basic rate)

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