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.
| System | Role | API Surface | Direction |
|---|---|---|---|
| Workday HCM / SF EC | HRIS — employee master, org structure, compensation | SOAP (WWS) / OData v4 | Outbound (source of truth) |
| ADP Workforce Now | Payroll provider — calc, tax, garnishments | REST API v2 (JSON) | Inbound + Outbound |
| Paylocity Web Pay | Payroll provider — calc, tax, garnishments | REST API (JSON) | Inbound + Outbound |
| Paychex Flex | Payroll provider — calc, tax, garnishments | REST API v1 (JSON) | Inbound + Outbound |
| SAP ECP | Native SF payroll — calc, tax, posting | RFC/IDoc/OData | Bidirectional with SF EC |
| Workday Payroll | Native Workday payroll — calc, tax, posting | Internal (no API needed) | Bidirectional with Workday HCM |
| Target ERP | Financial system — GL, AP, bank reconciliation | OData/REST/SOAP/File | Inbound (receives journal entries) |
| iPaaS | Orchestrator — transforms, error handling, scheduling | N/A | Orchestrator |
| API Surface | Provider | Protocol | Best For | Auth | Rate Limit | Bulk? |
|---|---|---|---|---|---|---|
| Workday Web Services (WWS) | Workday | SOAP/XML | Employee data, payroll results, journal entries | WS-Security + OAuth 2.0 | Fair use / throttled | Yes (batch) |
| Workday REST API | Workday | HTTPS/JSON | Reports-as-a-Service (RaaS), worker data | OAuth 2.0 (JWT bearer) | Fair use / throttled | Limited |
| ADP Pay Data Input v1 | ADP | HTTPS/JSON | Batch earnings, deductions, reimbursements | OAuth 2.0 client credentials | 30 req/min per endpoint | Yes (batch payloads) |
| ADP Payroll Output API | ADP | HTTPS/JSON | Pay statements, check-level detail | OAuth 2.0 client credentials | 30 req/min per endpoint | Yes |
| Paylocity Pay Setup API | Paylocity | HTTPS/JSON | Earnings codes, deduction codes, pay frequencies | API key + secret | 120 req/min | No |
| Paylocity Pay Statements API | Paylocity | HTTPS/JSON | Post-run pay data, GL summary | API key + secret | 120 req/min | Yes |
| Paychex Flex API | Paychex | HTTPS/JSON | Worker data, pay components, check data | OAuth 2.0 auth code | Per-agreement | Limited |
| SAP ECP Integration | SAP | RFC/IDoc + OData | EC-to-ECP replication, GL posting to S/4HANA | SAML/OAuth 2.0 | System-dependent | Yes (IDoc batch) |
| SF EC OData API | SAP | OData v4 | Employee master, compensation, job info | OAuth 2.0 SAML bearer | 300 req/min (concurrent: 10) | Yes (batch) |
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| ADP batch payload | 500 employees per batch | Pay Data Input v1 | Split larger payrolls into multiple batches |
| Paylocity response page size | 100 records per page | All list endpoints | Use pagination token for full dataset |
| Paychex response page size | 50 records default, 100 max | Worker/check endpoints | Set via limit query param |
| SAP ECP IDoc batch | 1,000 posting documents per run | GL posting to S/4HANA | Configure in posting run settings |
| Workday RaaS report | 100,000 rows per report execution | Custom reports | Use incremental/delta reports for larger datasets |
| Limit Type | Value | Window | Provider |
|---|---|---|---|
| ADP API calls | 30 requests/minute per endpoint | Per minute | ADP Workforce Now |
| Paylocity API calls | 120 requests/minute | Per minute | Paylocity Web Pay |
| SF EC OData concurrent | 10 concurrent requests | Per tenant | SAP SuccessFactors |
| SF EC OData rate | 300 requests/minute | Per tenant | SAP SuccessFactors |
| Workday | Fair use / throttled | Dynamic | Workday (no published hard limit) |
| Paychex | Per agreement | Per agreement | Paychex Flex |
| Provider | Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|---|
| ADP | OAuth 2.0 client credentials | Server-to-server integration | 60 minutes | New token per expiry | Requires ADP Marketplace registration |
| Paylocity | API key + secret | All API integrations | Session-based | Per request | Key/secret per company |
| Paychex | OAuth 2.0 authorization code | User-context or server integrations | 60 minutes | Yes (refresh token) | Sandbox available for testing |
| Workday | OAuth 2.0 JWT bearer / WS-Security | RaaS + REST / SOAP | Configurable (default 60 min) | Yes | ISU required for unattended |
| SAP SF | OAuth 2.0 SAML bearer assertion | OData API calls | 60 minutes | Yes | X.509 certificate for SAML |
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
| Step | Source | Action | Target | Data Objects | Timing | Failure Handling |
|---|---|---|---|---|---|---|
| 1 | HRIS | New hire / change event sync | Payroll Provider | Employee master: name, SSN, tax elections, direct deposit, org | Real-time or daily batch | Retry 3x, alert payroll admin |
| 2 | Time & Attendance | Hours, OT, PTO exported | Payroll Provider | Timecard data by earnings code | Pre-payroll (1-2 days before run) | Manual entry fallback |
| 3 | Payroll Provider | Payroll calculation runs | Internal | Gross-to-net: earnings, taxes, deductions, garnishments | Payroll schedule | Re-run with corrections |
| 4 | Payroll Provider | Results extracted via API | iPaaS | Pay statements, GL summary, tax deposits | Post-run (within hours) | Retry API, file export fallback |
| 5 | iPaaS | Transform to GL journal format | Target ERP | Journal entry: account, amount, D/C, entity, cost center | Post-run (same day) | DLQ + manual journal |
| 6 | iPaaS | Post journal entry to ERP GL | Target ERP | GL journal entry (summarized or detailed) | Before period close | Retry, validation error alert |
| 7 | Payroll Provider | Generate bank payment file | Bank | ACH/BACS/SEPA payment file | Before bank cutoff | Re-generate, manual wire |
| 8 | Payroll Provider | File tax returns/deposits | Tax Authorities | 941, SUI, state withholding, W-2/1099 | Per statutory deadline | Manual filing, extension |
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.
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.
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.
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.
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.
| Source Field (HRIS) | Target Field (Payroll) | Type | Transform | Gotcha |
|---|---|---|---|---|
| Employee ID | External ID / Badge Number | String | Direct | ADP uses associateOID internally |
| Legal First Name | First Name / Given Name | String | Direct | Paylocity has 40-char limit; Workday allows 100 |
| SSN / National ID | SSN / Tax ID | Encrypted String | Encrypt in transit | Different masking rules per provider; never log |
| Hire Date | Original Hire Date | Date (ISO 8601) | Direct | Rehire: some providers need both original + rehire date |
| Annual Salary | Compensation Rate | Decimal | Convert to pay-period | Must match pay frequency: /26 (bi-weekly) or /24 (semi-monthly) |
| Cost Center | Department Code | String | Cross-reference table | HRIS cost center IDs rarely match payroll dept codes |
| Work Location (State) | Tax Jurisdiction | String/Code | Map to FIPS/jurisdiction | Remote workers: may need resident + work state |
| Federal Filing Status | W-4 Filing Status | Code | Provider-specific code | Post-2020 W-4 uses different fields than legacy |
| Direct Deposit | Bank Account + Routing | Encrypted | Provider-specific format | ADP: separate endpoint; Paylocity: within employee record |
| Payroll Component | GL Account (Typical) | Debit/Credit | Category | Notes |
|---|---|---|---|---|
| Regular Wages | 6100 Salary Expense | Debit | Earnings | Split by department/cost center |
| Overtime | 6110 Overtime Expense | Debit | Earnings | Track FLSA compliance |
| Bonus | 6120 Bonus Expense | Debit | Earnings | May post to different period than paid |
| Employer FICA | 6200 Payroll Tax Expense | Debit | Employer Tax | 7.65% up to SS wage base |
| Employer FUTA | 6210 FUTA Expense | Debit | Employer Tax | 0.6% on first $7,000/employee |
| Employer Health Ins. | 6300 Benefits Expense | Debit | Employer Benefit | Employer portion only |
| Employee Federal Tax | 2200 Federal Tax Payable | Credit | Liability | Due per IRS deposit schedule |
| Employee State Tax | 2210 State Tax Payable | Credit | Liability | Due per state schedule |
| Employee FICA | 2220 FICA Payable | Credit | Liability | Combined with employer for deposit |
| Employee 401k | 2300 401k Payable | Credit | Liability | Remit to plan administrator |
| Net Pay | 1000 Cash / Payroll Bank | Credit | Asset | ACH/check amount to employees |
| Code/Error | Provider | Meaning | Resolution |
|---|---|---|---|
| 429 Too Many Requests | All REST APIs | Rate limit exceeded | Exponential backoff: wait 2^n seconds, max 5 retries |
| 401 Unauthorized | All | Token expired or invalid | Refresh token; check ISU/API user status |
| INVALID_FIELD_VALUE | ADP | Field validation failure | Check field spec in ADP API docs; validate before send |
| DUPLICATE_RECORD | Paylocity | Employee already exists | Check existing records first; use upsert pattern |
| POSTING_PERIOD_CLOSED | SAP S/4HANA | GL period already closed | Open special period or post to next period with accrual |
| UNBALANCED_ENTRY | ERP (all) | Debits != Credits | Add rounding adjustment line; re-validate mappings |
| INVALID_COST_CENTER | ERP (all) | Cost center not in master data | Sync org structure before payroll GL posting |
| BANK_REJECT_R03 | ACH/NACHA | No account / unable to locate | Contact employee, update direct deposit info |
Monitor for unmapped codes every payroll run; alert on catch-all account postings. [src6]Build entity split logic based on transfer effective date. [src7]Implement reversal-and-repost pattern for prior period corrections. [src7]Automate submission immediately after generation; build 1-hour buffer; manual wire fallback. [src8]Maintain work-state + resident-state mapping; update on relocation. [src8]Token refresh with 5-min pre-expiry buffer; checkpoint progress for restart. [src2]# 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
# 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
# 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.
# 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
# BAD -- posts invalid journal, requires manual reversal
journal = build_journal_from_payroll(payroll_data)
erp_client.post_journal(journal) # May fail with unbalanced entry
# 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)
Derive per-period amount at sync time using provider's pay frequency code. [src6]Request full sandbox refresh; use synthetic data for edge cases. [src2, src3]Use check date (pay date) as GL posting date per your accounting policy. [src7]Always extract both employee and employer portions. [src7]Track expiration dates; rotate 30 days early; test in sandbox first. [src1, src4]Tag each payroll run with type and apply appropriate GL mapping. [src7]# 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":...}]}
| Provider / API | Current Version | Last Major Change | Status | Notes |
|---|---|---|---|---|
| ADP API v2 | v2 | 2024-Q3 (Pay Data Input v2 beta) | GA | v1 deprecated for new integrations |
| Paylocity API | v2025-03-11 | 2025-Q1 (versioned URLs) | GA | Dated releases, 12-month backward compat |
| Paychex Flex API | v1 | 2024 (webhooks added) | GA | Sandbox available |
| Workday Web Services | 2025R2 (v43.x) | 2025-09 (REST expansion) | GA | Support drops 3+ releases old |
| SAP SF EC OData | v4 | 2025 H2 (new compensation entities) | GA | Check SAP API Business Hub |
| SAP ECP Integration | S/4HANA 2023+ | 2024 (cloud-native posting) | GA | Web service posting recommended over IDoc |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Running HRIS + separate payroll provider, need automated GL posting | Using native payroll with same-vendor ERP | Vendor's built-in GL posting config |
| Multi-country payroll across 5+ countries | Single-country with built-in GL export | Provider's native GL file export + ERP import |
| 500+ employees with complex earnings/deduction structures | < 50 employees with simple payroll | Provider's built-in QuickBooks/Xero sync |
| Compliance requires detailed audit trail | Manual monthly journals acceptable | Manual process with payroll register |
| Capability | Workday Payroll | SAP ECP | ADP WFN | Paylocity | Paychex Flex |
|---|---|---|---|---|---|
| Native HRIS Integration | Built-in (Workday HCM) | Built-in (SF EC) | API required | API required | API required |
| GL Posting Method | FA journal task | IDoc/web service to S/4 | API extract + external post | API extract + external post | API extract + external post |
| Multi-Country | 50+ countries | 50+ countries | 140+ via partners | US + Canada | US only |
| API Style | SOAP + REST | OData + RFC/IDoc | REST (JSON) | REST (JSON) | REST (JSON) |
| Rate Limit | Fair use | Tenant-based | 30 req/min | 120 req/min | Per agreement |
| Tax Filing | Included | Included | Included | Included | Included |
| Bank File Generation | Included | Included | Included | Included | Included |
| Sandbox | Preview tenant | Test instance | Available | Available | Available |
| Typical Size | 1,000+ employees | 5,000+ employees | 50-50,000 | 50-5,000 | 10-1,000 |