HRIS-to-Payroll-to-GL Integration Playbook

Type: ERP Integration Systems: Workday, SuccessFactors, ADP, Paylocity, Paychex Confidence: 0.85 Sources: 8 Verified: 2026-03-03 Freshness: current

TL;DR

System Profile

This playbook covers the end-to-end integration flow from HRIS systems (Workday HCM, SAP SuccessFactors Employee Central) through payroll providers (ADP, Paylocity, Paychex, or native Workday/SAP payroll) to ERP general ledger posting. It applies to both single-country (US) and multi-country deployments. The playbook assumes a middleware/iPaaS layer (MuleSoft, Workato, Boomi, or custom) orchestrating the flow.

SystemRoleAPI SurfaceDirection
Workday HCM / SF ECHRIS — employee master, org structure, compensationSOAP (WWS) / OData v4Outbound (source of truth)
ADP Workforce NowPayroll provider — calc, tax, garnishmentsREST API v2 (JSON)Inbound + Outbound
Paylocity Web PayPayroll provider — calc, tax, garnishmentsREST API (JSON)Inbound + Outbound
Paychex FlexPayroll provider — calc, tax, garnishmentsREST API v1 (JSON)Inbound + Outbound
SAP ECPNative SF payroll — calc, tax, postingRFC/IDoc/ODataBidirectional with SF EC
Workday PayrollNative Workday payroll — calc, tax, postingInternal (no API needed)Bidirectional with Workday HCM
Target ERPFinancial system — GL, AP, bank reconciliationOData/REST/SOAP/FileInbound (receives journal entries)
iPaaSOrchestrator — transforms, error handling, schedulingN/AOrchestrator

API Surfaces & Capabilities

API SurfaceProviderProtocolBest ForAuthRate LimitBulk?
Workday Web Services (WWS)WorkdaySOAP/XMLEmployee data, payroll results, journal entriesWS-Security + OAuth 2.0Fair use / throttledYes (batch)
Workday REST APIWorkdayHTTPS/JSONReports-as-a-Service (RaaS), worker dataOAuth 2.0 (JWT bearer)Fair use / throttledLimited
ADP Pay Data Input v1ADPHTTPS/JSONBatch earnings, deductions, reimbursementsOAuth 2.0 client credentials30 req/min per endpointYes (batch payloads)
ADP Payroll Output APIADPHTTPS/JSONPay statements, check-level detailOAuth 2.0 client credentials30 req/min per endpointYes
Paylocity Pay Setup APIPaylocityHTTPS/JSONEarnings codes, deduction codes, pay frequenciesAPI key + secret120 req/minNo
Paylocity Pay Statements APIPaylocityHTTPS/JSONPost-run pay data, GL summaryAPI key + secret120 req/minYes
Paychex Flex APIPaychexHTTPS/JSONWorker data, pay components, check dataOAuth 2.0 auth codePer-agreementLimited
SAP ECP IntegrationSAPRFC/IDoc + ODataEC-to-ECP replication, GL posting to S/4HANASAML/OAuth 2.0System-dependentYes (IDoc batch)
SF EC OData APISAPOData v4Employee master, compensation, job infoOAuth 2.0 SAML bearer300 req/min (concurrent: 10)Yes (batch)

Rate Limits & Quotas

Per-Request Limits

Limit TypeValueApplies ToNotes
ADP batch payload500 employees per batchPay Data Input v1Split larger payrolls into multiple batches
Paylocity response page size100 records per pageAll list endpointsUse pagination token for full dataset
Paychex response page size50 records default, 100 maxWorker/check endpointsSet via limit query param
SAP ECP IDoc batch1,000 posting documents per runGL posting to S/4HANAConfigure in posting run settings
Workday RaaS report100,000 rows per report executionCustom reportsUse incremental/delta reports for larger datasets

Rolling / Daily Limits

Limit TypeValueWindowProvider
ADP API calls30 requests/minute per endpointPer minuteADP Workforce Now
Paylocity API calls120 requests/minutePer minutePaylocity Web Pay
SF EC OData concurrent10 concurrent requestsPer tenantSAP SuccessFactors
SF EC OData rate300 requests/minutePer tenantSAP SuccessFactors
WorkdayFair use / throttledDynamicWorkday (no published hard limit)
PaychexPer agreementPer agreementPaychex Flex

Authentication

ProviderFlowUse WhenToken LifetimeRefresh?Notes
ADPOAuth 2.0 client credentialsServer-to-server integration60 minutesNew token per expiryRequires ADP Marketplace registration
PaylocityAPI key + secretAll API integrationsSession-basedPer requestKey/secret per company
PaychexOAuth 2.0 authorization codeUser-context or server integrations60 minutesYes (refresh token)Sandbox available for testing
WorkdayOAuth 2.0 JWT bearer / WS-SecurityRaaS + REST / SOAPConfigurable (default 60 min)YesISU required for unattended
SAP SFOAuth 2.0 SAML bearer assertionOData API calls60 minutesYesX.509 certificate for SAML

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

START -- Payroll Integration Architecture
|
+-- What is the HRIS-to-Payroll relationship?
|   +-- Native payroll (Workday Payroll, SAP ECP)
|   |   +-- No integration needed for employee data
|   |   +-- GL posting: Workday = Financial Accounting journal task
|   |   +-- GL posting: SAP ECP = IDoc/posting run to S/4HANA
|   +-- Third-party payroll (ADP, Paylocity, Paychex)
|   |   +-- Employee master sync: HRIS --> Payroll (new hires, terms, comp changes)
|   |   +-- Pay data input: Time --> Payroll (hours, earnings, deductions)
|   |   +-- Payroll results: extract via API for GL posting
|   +-- Global payroll aggregator (CloudPay, Papaya, Deel)
|       +-- Single API for multi-country
|       +-- Aggregator handles in-country providers
|
+-- PAYROLL RESULTS: How to extract?
|   +-- ADP: Payroll Output API (pay statements, check-level detail)
|   +-- Paylocity: PayStatements API (summary + detail)
|   +-- Paychex: Check endpoint (pay component breakdown)
|   +-- All: flat file export (CSV) as fallback
|
+-- GL POSTING: How does payroll reach ERP?
|   +-- Summarized journal (most common): 1 entry per run per entity
|   +-- Detailed journal: 1 line per employee per code (high volume)
|   +-- File-based: CSV/XML import (SAP IDoc, Oracle FBDI, D365 data entities)
|
+-- BANK FILE: Payment to employees
    +-- US: ACH (NACHA) | UK: BACS | EU: SEPA SCT | Other: country-specific

Quick Reference: Integration Flow

StepSourceActionTargetData ObjectsTimingFailure Handling
1HRISNew hire / change event syncPayroll ProviderEmployee master: name, SSN, tax elections, direct deposit, orgReal-time or daily batchRetry 3x, alert payroll admin
2Time & AttendanceHours, OT, PTO exportedPayroll ProviderTimecard data by earnings codePre-payroll (1-2 days before run)Manual entry fallback
3Payroll ProviderPayroll calculation runsInternalGross-to-net: earnings, taxes, deductions, garnishmentsPayroll scheduleRe-run with corrections
4Payroll ProviderResults extracted via APIiPaaSPay statements, GL summary, tax depositsPost-run (within hours)Retry API, file export fallback
5iPaaSTransform to GL journal formatTarget ERPJournal entry: account, amount, D/C, entity, cost centerPost-run (same day)DLQ + manual journal
6iPaaSPost journal entry to ERP GLTarget ERPGL journal entry (summarized or detailed)Before period closeRetry, validation error alert
7Payroll ProviderGenerate bank payment fileBankACH/BACS/SEPA payment fileBefore bank cutoffRe-generate, manual wire
8Payroll ProviderFile tax returns/depositsTax Authorities941, SUI, state withholding, W-2/1099Per statutory deadlineManual filing, extension

Step-by-Step Integration Guide

1. Set up employee master data sync (HRIS → Payroll)

Configure the HRIS to push new hires, terminations, compensation changes, and organizational reassignments to the payroll provider. For ADP, use the Worker v2 API; for Paylocity, use the Employee (v2) endpoint; for native payroll, sync is automatic. [src1, src2, src3]

# Sync new hire from Workday to ADP via iPaaS
import requests

# Get Workday employee data via RaaS
workday_url = "https://{tenant}.workday.com/ccx/service/customreport2/{tenant}/{owner}/{report}?format=json"
employee_data = requests.get(workday_url, headers={"Authorization": f"Bearer {workday_token}"}).json()

# Transform to ADP format and create worker
adp_payload = {"events": [{"data": {"transform": {"worker": {
    "person": {"legalName": {"givenName": employee_data["First_Name"],
                              "familyName1": employee_data["Last_Name"]}},
    "workerDates": {"originalHireDate": employee_data["Hire_Date"]}
}}}}]}
response = requests.post("https://api.adp.com/hr/v2/workers",
    json=adp_payload, headers={"Authorization": f"Bearer {adp_token}"})

Verify: GET /hr/v2/workers/{aoid} → worker status = "Active", hire date matches.

2. Configure earnings and deduction code mapping

Map HRIS compensation elements to payroll provider earnings/deduction codes. Pull available codes from the payroll provider API and build a cross-reference table. [src2, src3]

# Pull Paylocity earnings codes and build mapping
earnings_codes = requests.get(
    f"https://api.paylocity.com/api/v2/companies/{company_id}/earnings",
    headers={"Authorization": f"Bearer {token}"}
).json()

EARNINGS_MAP = {
    "Regular":    {"code": "REG",  "category": "E", "gl_account": "6100-100"},
    "Overtime":   {"code": "OT",   "category": "E", "gl_account": "6100-110"},
    "Bonus":      {"code": "BON",  "category": "E", "gl_account": "6100-200"},
    "FedTax":     {"code": "FIT",  "category": "T", "gl_account": "2200-100"},
    "FICA_SS_EE": {"code": "SS",   "category": "T", "gl_account": "2200-300"},
}

Verify: Every HRIS earnings type has a valid payroll code and GL account mapped.

3. Extract payroll results post-run

After each payroll run completes, extract results via API to build GL journal entries. [src2, src3, src5]

# Extract ADP payroll results for GL posting
payroll_data = requests.get("https://api.adp.com/payroll/v1/payroll-output",
    headers={"Authorization": f"Bearer {adp_token}"},
    params={"payPeriod.endDate": "2026-02-28"}).json()

# Aggregate by GL account for journal entry
gl_entries = {}
for statement in payroll_data.get("payStatements", []):
    for earning in statement.get("earnings", []):
        code = earning["earningCode"]["codeValue"]
        amount = float(earning["amount"]["amountValue"])
        gl_account = EARNINGS_MAP.get(code, {}).get("gl_account", "6100-999")
        key = (gl_account, statement.get("costCenter", "0000"), "D")
        gl_entries[key] = gl_entries.get(key, 0) + amount

Verify: Total debits = total credits. Net pay + taxes + deductions = gross pay.

4. Build and post GL journal entries to ERP

Transform aggregated payroll data into the ERP's journal entry format and post. [src6, src7]

# Post payroll journal to SAP S/4HANA via OData
journal_items = []
for (gl_account, cost_center, dc_indicator), amount in gl_entries.items():
    journal_items.append({
        "CompanyCode": "1000", "GLAccount": gl_account.replace("-", ""),
        "CostCenter": cost_center, "AmountInTransactionCurrency": str(amount),
        "TransactionCurrency": "USD", "DebitCreditCode": dc_indicator,
        "PostingDate": "2026-02-28", "DocumentHeaderText": "Payroll Feb 2026 P2"
    })
response = requests.post(s4_url, json={"JournalEntryItemBasic": journal_items},
    headers={"Authorization": f"Bearer {s4_token}", "X-CSRF-Token": csrf_token})

Verify: GET /JournalEntryItemBasic?$filter=AccountingDocument eq '{doc}' → all lines posted, debits = credits.

5. Validate and reconcile

Run a 3-way reconciliation: payroll provider gross-to-net, GL journal entry totals, and bank file net pay total. [src7]

# 3-way reconciliation check
assert abs(gl_total_debits - gl_total_credits) < 0.01, "GL out of balance"
assert abs(payroll_net - bank_file_total) < 0.01, "Bank file mismatch"
assert abs(payroll_gross - (payroll_net + payroll_taxes + payroll_deductions)) < 0.01, "Gross-to-net mismatch"

Verify: All three assertions pass. Investigate specific discrepancy category if any fail.

Data Mapping

HRIS → Payroll Provider Field Mapping

Source Field (HRIS)Target Field (Payroll)TypeTransformGotcha
Employee IDExternal ID / Badge NumberStringDirectADP uses associateOID internally
Legal First NameFirst Name / Given NameStringDirectPaylocity has 40-char limit; Workday allows 100
SSN / National IDSSN / Tax IDEncrypted StringEncrypt in transitDifferent masking rules per provider; never log
Hire DateOriginal Hire DateDate (ISO 8601)DirectRehire: some providers need both original + rehire date
Annual SalaryCompensation RateDecimalConvert to pay-periodMust match pay frequency: /26 (bi-weekly) or /24 (semi-monthly)
Cost CenterDepartment CodeStringCross-reference tableHRIS cost center IDs rarely match payroll dept codes
Work Location (State)Tax JurisdictionString/CodeMap to FIPS/jurisdictionRemote workers: may need resident + work state
Federal Filing StatusW-4 Filing StatusCodeProvider-specific codePost-2020 W-4 uses different fields than legacy
Direct DepositBank Account + RoutingEncryptedProvider-specific formatADP: separate endpoint; Paylocity: within employee record

Payroll Output → GL Account Mapping

Payroll ComponentGL Account (Typical)Debit/CreditCategoryNotes
Regular Wages6100 Salary ExpenseDebitEarningsSplit by department/cost center
Overtime6110 Overtime ExpenseDebitEarningsTrack FLSA compliance
Bonus6120 Bonus ExpenseDebitEarningsMay post to different period than paid
Employer FICA6200 Payroll Tax ExpenseDebitEmployer Tax7.65% up to SS wage base
Employer FUTA6210 FUTA ExpenseDebitEmployer Tax0.6% on first $7,000/employee
Employer Health Ins.6300 Benefits ExpenseDebitEmployer BenefitEmployer portion only
Employee Federal Tax2200 Federal Tax PayableCreditLiabilityDue per IRS deposit schedule
Employee State Tax2210 State Tax PayableCreditLiabilityDue per state schedule
Employee FICA2220 FICA PayableCreditLiabilityCombined with employer for deposit
Employee 401k2300 401k PayableCreditLiabilityRemit to plan administrator
Net Pay1000 Cash / Payroll BankCreditAssetACH/check amount to employees

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

Code/ErrorProviderMeaningResolution
429 Too Many RequestsAll REST APIsRate limit exceededExponential backoff: wait 2^n seconds, max 5 retries
401 UnauthorizedAllToken expired or invalidRefresh token; check ISU/API user status
INVALID_FIELD_VALUEADPField validation failureCheck field spec in ADP API docs; validate before send
DUPLICATE_RECORDPaylocityEmployee already existsCheck existing records first; use upsert pattern
POSTING_PERIOD_CLOSEDSAP S/4HANAGL period already closedOpen special period or post to next period with accrual
UNBALANCED_ENTRYERP (all)Debits != CreditsAdd rounding adjustment line; re-validate mappings
INVALID_COST_CENTERERP (all)Cost center not in master dataSync org structure before payroll GL posting
BANK_REJECT_R03ACH/NACHANo account / unable to locateContact employee, update direct deposit info

Failure Points in Production

Anti-Patterns

Wrong: Post one GL journal entry per employee per pay component

# BAD -- creates thousands of GL lines, overwhelms ERP
for employee in payroll_results:
    for component in employee["pay_components"]:
        post_journal_entry(account=component["gl_account"], amount=component["amount"])
# 1,000 employees x 15 components = 15,000 individual journal entries

Correct: Summarize by GL account + cost center, one journal per run per entity

# GOOD -- aggregates to manageable size, standard practice
journal_lines = {}
for employee in payroll_results:
    for component in employee["pay_components"]:
        key = (component["gl_account"], employee["cost_center"], component["dc"], employee["entity"])
        journal_lines[key] = journal_lines.get(key, 0) + component["amount"]
# 1 journal entry per entity, typically 30-80 lines

Wrong: Hardcode earnings/deduction code mappings

# BAD -- breaks when provider adds/changes codes
def map_earning(code):
    if code == "REG": return "6100"
    if code == "OT": return "6110"
    # New state sick leave code? Not handled. Data lost.

Correct: Configurable mapping table with fallback alerting

# GOOD -- external config, unknown-code alerting
EARNINGS_MAP = json.load(open("earnings_gl_map.json"))
def map_earning(code):
    mapping = EARNINGS_MAP.get(code)
    if not mapping:
        alert_payroll_team(f"Unmapped earnings code: {code}")
        return {"gl_account": "6199-000"}  # Catch-all with alert
    return mapping

Wrong: Post GL journal without pre-validation

# BAD -- posts invalid journal, requires manual reversal
journal = build_journal_from_payroll(payroll_data)
erp_client.post_journal(journal)  # May fail with unbalanced entry

Correct: Validate before posting with dry-run

# GOOD -- catches errors before they hit the ERP
journal = build_journal_from_payroll(payroll_data)
errors = validate_journal(journal)  # Check balance, GL accounts, cost centers
if errors:
    alert_finance_team(errors); log_to_dlq(journal, errors)
else:
    erp_client.post_journal(journal)

Common Pitfalls

Diagnostic Commands

# ADP: Test authentication
curl -X POST "https://accounts.adp.com/auth/oauth/v2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=YOUR_ID&client_secret=YOUR_SECRET"
# Expected: {"access_token":"...","token_type":"Bearer","expires_in":3600}

# ADP: Get worker list
curl -X GET "https://api.adp.com/hr/v2/workers" \
  -H "Authorization: Bearer {token}"
# Expected: {"workers":[{"associateOID":"..."}]}

# Paylocity: Get earnings codes
curl -X GET "https://api.paylocity.com/api/v2/companies/{companyId}/earnings" \
  -H "Authorization: Bearer {token}"
# Expected: [{"earningsCode":"REG","description":"Regular Pay",...}]

# SAP S/4HANA: Verify GL account exists
curl -X GET "https://{host}/sap/opu/odata/sap/API_JOURNALENTRYITEMBASIC_SRV/A_GLAccountInChartOfAccounts('6100')" \
  -H "Authorization: Bearer {token}"
# Expected: {"d":{"GLAccount":"6100","GLAccountName":"Salary Expense"}}

# Workday: Test RaaS payroll journal report
curl -X GET "https://{tenant}.workday.com/ccx/service/customreport2/{tenant}/{owner}/Payroll_GL_Journal?format=json" \
  -H "Authorization: Bearer {token}"
# Expected: {"Report_Entry":[{"Journal_Line":...}]}

Version History & Compatibility

Provider / APICurrent VersionLast Major ChangeStatusNotes
ADP API v2v22024-Q3 (Pay Data Input v2 beta)GAv1 deprecated for new integrations
Paylocity APIv2025-03-112025-Q1 (versioned URLs)GADated releases, 12-month backward compat
Paychex Flex APIv12024 (webhooks added)GASandbox available
Workday Web Services2025R2 (v43.x)2025-09 (REST expansion)GASupport drops 3+ releases old
SAP SF EC ODatav42025 H2 (new compensation entities)GACheck SAP API Business Hub
SAP ECP IntegrationS/4HANA 2023+2024 (cloud-native posting)GAWeb service posting recommended over IDoc

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Running HRIS + separate payroll provider, need automated GL postingUsing native payroll with same-vendor ERPVendor's built-in GL posting config
Multi-country payroll across 5+ countriesSingle-country with built-in GL exportProvider's native GL file export + ERP import
500+ employees with complex earnings/deduction structures< 50 employees with simple payrollProvider's built-in QuickBooks/Xero sync
Compliance requires detailed audit trailManual monthly journals acceptableManual process with payroll register

Cross-System Comparison

CapabilityWorkday PayrollSAP ECPADP WFNPaylocityPaychex Flex
Native HRIS IntegrationBuilt-in (Workday HCM)Built-in (SF EC)API requiredAPI requiredAPI required
GL Posting MethodFA journal taskIDoc/web service to S/4API extract + external postAPI extract + external postAPI extract + external post
Multi-Country50+ countries50+ countries140+ via partnersUS + CanadaUS only
API StyleSOAP + RESTOData + RFC/IDocREST (JSON)REST (JSON)REST (JSON)
Rate LimitFair useTenant-based30 req/min120 req/minPer agreement
Tax FilingIncludedIncludedIncludedIncludedIncluded
Bank File GenerationIncludedIncludedIncludedIncludedIncluded
SandboxPreview tenantTest instanceAvailableAvailableAvailable
Typical Size1,000+ employees5,000+ employees50-50,00050-5,00010-1,000

Important Caveats

Related Units