Hire-to-Retire integration is a cross-system integration playbook that orchestrates data flow across 4-6 systems covering the complete employee lifecycle: recruitment, onboarding, core HR, time and attendance, payroll processing, benefits administration, GL posting, and separation/offboarding. The HRIS/HCM platform serves as the system of record for employee master data, and all other systems are consumers. This card covers the four most common enterprise HCM platforms and their integration with external payroll providers, benefits platforms, and ERP financial systems. [src1]
| System | Role | API Surface | Direction |
|---|---|---|---|
| HRIS/HCM (Workday, SAP SF, Oracle HCM, D365 HR) | Employee master — source of truth | REST, SOAP, OData, RaaS | Outbound (publishes events) |
| Payroll Provider (ADP, Ceridian, native, bureau) | Compensation processing — gross-to-net, tax | SFTP/CSV, REST API, batch files | Inbound (consumes employee + time data) |
| Benefits Platform (PlanSource, bswift, EmployeeNavigator) | Enrollment, eligibility, carrier feeds | REST API, EDI 834, SFTP/CSV | Bidirectional |
| ERP / Finance (SAP S/4HANA, Oracle ERP, NetSuite, D365 Finance) | GL posting — labor cost allocation | OData, REST, BAPI/IDoc, journal import | Inbound (consumes payroll results) |
| Identity Provider (Okta, Azure AD/Entra, Ping) | Access provisioning | SCIM 2.0, REST | Inbound (consumes hire/term events) |
| Middleware / iPaaS (MuleSoft, Boomi, Workato, SAP IS) | Orchestration, transformation, error handling | N/A | Orchestrator |
| Platform | API Surface | Protocol | Best For | Rate Limit | Auth Method |
|---|---|---|---|---|---|
| Workday REST | HTTPS/JSON | REST | Individual worker lookups, real-time events | 60 req/min per resource type | OAuth 2.0 |
| Workday SOAP | HTTPS/XML | SOAP | Bulk worker data, legacy integrations | 1,000 req/min per tenant | ISU |
| Workday RaaS | HTTPS/JSON,CSV,XML | REST | Custom report extraction, batch data pulls | Shared with SOAP limit | ISU or OAuth 2.0 |
| SAP SF OData v2 | HTTPS/JSON,XML | OData | Employee Central CRUD, real-time sync | 200 concurrent connections | OAuth 2.0 + SAML |
| SAP SF SFAPI (SOAP) | HTTPS/XML | SOAP | Legacy bulk operations | Deprecated for new dev | Basic Auth |
| Oracle HCM REST | HTTPS/JSON | REST | Worker, assignment, compensation data | Configurable per tenant | OAuth 2.0 |
| Oracle HCM FBDI | File-based (CSV) | File | Bulk data import (payroll, journal) | N/A | OAuth 2.0 |
| D365 HR Dataverse | OData v4 / HTTPS | OData | Employee entity CRUD, Power Platform | 6,000 req/5min per user | OAuth 2.0 (Azure AD) |
| ADP API | HTTPS/JSON | REST | Worker, payroll data, pay statements | Varies by product tier | OAuth 2.0 |
| Generic Payroll SFTP | SFTP/CSV | File | Batch payroll input files | N/A | SSH keys + PGP |
| Limit Type | Platform | Value | Notes |
|---|---|---|---|
| Max records per page | Workday REST | 100 | Use offset pagination |
| Max records per RaaS report | Workday RaaS | 2,000 per batch | Paginate with page parameter |
| Max records per OData query | SAP SuccessFactors | 1,000 | Use $skiptoken for pagination |
| Max FBDI file size | Oracle HCM | 250 MB | Split larger payroll files |
| Max Dataverse batch size | D365 HR | 1,000 operations | Use $batch endpoint |
| Max payload size | Workday REST | 5 MB | For worker create/update operations |
| Limit Type | Platform | Value | Window |
|---|---|---|---|
| REST API calls | Workday | 60/min per resource | Per-minute rolling |
| SOAP API calls | Workday | 1,000/min per tenant | Per-minute rolling |
| Concurrent connections | SAP SuccessFactors | 200 per company instance | Concurrent |
| API calls | D365 HR (Dataverse) | 6,000 per 5 min per user | 5-min sliding window |
| Concurrent long-running | Oracle HCM | 25 per pod | Concurrent |
| SFTP sessions | Most payroll providers | 1-5 concurrent | Per account |
| Platform | Flow | Use When | Token Lifetime | Refresh? |
|---|---|---|---|---|
| Workday | OAuth 2.0 (Authorization Code) | User-context integrations | 1 hour | Yes |
| Workday | ISU (Integration System User) | Server-to-server batch, RaaS | Session-based | New credentials per request |
| SAP SuccessFactors | OAuth 2.0 + SAML assertion | API access to Employee Central | Configurable (default 1h) | Yes |
| Oracle HCM | OAuth 2.0 (Client Credentials) | Server-to-server | 1 hour | Yes |
| D365 HR | OAuth 2.0 (Azure AD / Entra) | All Dataverse API access | 1 hour | Yes |
| Payroll SFTP | SSH key pair + PGP | Batch file transfer to payroll bureaus | N/A | N/A |
START — Organization needs Hire-to-Retire integration
├── Is payroll native to the HRIS platform?
│ ├── YES → STOP — this is configuration, not integration
│ └── NO → Continue
├── What type of payroll provider?
│ ├── Cloud payroll with REST API (ADP, Ceridian)
│ │ ├── Event-driven: HRIS publishes events → iPaaS → payroll API
│ │ └── Supplement with scheduled batch for time data
│ ├── Payroll bureau (no API, file-based only)
│ │ └── Batch/file: HRIS → iPaaS → CSV → PGP → SFTP
│ └── On-premise payroll (SAP, Oracle)
│ └── Middleware: SAP IS / Oracle OIC / MuleSoft → IDoc/BAPI/FBDI
├── Benefits integration?
│ ├── Native in HRIS → configuration only
│ ├── Third-party (PlanSource, bswift)
│ │ ├── Eligibility: HRIS → iPaaS → benefits API (real-time)
│ │ ├── Enrollments: benefits → iPaaS → payroll deductions (batch)
│ │ └── Carrier feeds: benefits → EDI 834 → carriers
│ └── Broker-managed → CSV via SFTP
├── ERP / Finance GL posting?
│ ├── Payroll results → iPaaS → journal entry → ERP GL (batch)
│ └── Labor cost allocation: HRIS org → ERP chart of accounts
└── Identity / IT provisioning?
├── Hire event → SCIM 2.0 → IdP → app provisioning
└── Term event → SCIM 2.0 → IdP → deprovisioning (same-day)
| Step | Source System | Action | Target System | Data Objects | Timing | Failure Handling |
|---|---|---|---|---|---|---|
| 1 | Recruiter / ATS | Candidate accepted → create employee | HRIS | Employee master, job, comp | Real-time (event) | Retry 3x, manual review |
| 2 | HRIS | New hire event | Identity Provider (SCIM) | User account, role, email | Real-time (< 1 hour) | Alert IT helpdesk |
| 3 | HRIS | Eligibility event | Benefits Platform | Demographics, job, dependents | Real-time or daily batch | Queue for next batch |
| 4 | Benefits Platform | Enrollment elections | Payroll | Deduction codes, amounts, dates | Batch (pre-cut-off) | Hold; escalate near cut-off |
| 5 | HRIS + Time System | Employee + time data | Payroll | Hours, earnings, deductions, tax | Batch (per pay period) | Block payroll run; alert HR |
| 6 | Payroll | Payroll results (gross-to-net) | ERP / Finance | Journal entries, cost allocations | Batch (post-payroll) | Retry; hold GL close |
| 7 | Payroll | Pay statements | Employee Self-Service | Pay stubs, tax forms | Post-payroll | Non-blocking; retry |
| 8 | HRIS | Termination event | All downstream systems | Separation date, final pay flag | Real-time + batch | Escalate: missed term = security risk |
Map employee identifiers across all systems. The HRIS employee ID is the master key; create a persistent cross-reference table in your middleware. [src5]
CREATE TABLE employee_xref (
hris_employee_id VARCHAR(50) PRIMARY KEY,
payroll_employee_id VARCHAR(20) NOT NULL,
benefits_member_id VARCHAR(30),
erp_person_id VARCHAR(30),
idp_user_id VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_xref_payroll ON employee_xref(payroll_employee_id);
CREATE INDEX idx_xref_benefits ON employee_xref(benefits_member_id);
Verify: SELECT COUNT(*) FROM employee_xref WHERE payroll_employee_id IS NULL; → expected: 0
Set up Workday Business Process notifications to publish employee lifecycle events to your iPaaS. [src2]
import requests
from datetime import datetime, timedelta
WORKDAY_TENANT = "your_tenant"
RAAS_ENDPOINT = f"https://wd5-services1.myworkday.com/{WORKDAY_TENANT}/customreport2/ISU_User/New_Hires_Report"
params = {
"Effective_Date": (datetime.utcnow() - timedelta(days=1)).strftime("%Y-%m-%d"),
"format": "json"
}
headers = {"Authorization": "Bearer <oauth_token>"}
response = requests.get(RAAS_ENDPOINT, params=params, headers=headers)
response.raise_for_status()
new_hires = response.json().get("Report_Entry", [])
Verify: len(new_hires) → expected: matches new hires in Workday for the date range
Transform HRIS employee data into payroll-provider format. Most payroll providers accept CSV via SFTP. [src3]
import csv, io, paramiko, gnupg
def transform_for_payroll(hris_records):
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["EMP_ID", "LAST_NAME", "FIRST_NAME", "SSN_ENCRYPTED",
"HIRE_DATE", "PAY_FREQUENCY", "SALARY_ANNUAL",
"FED_FILING_STATUS", "STATE_CODE", "COST_CENTER",
"DEPARTMENT_CODE", "EFFECTIVE_DATE"])
for emp in hris_records:
writer.writerow([emp["payroll_id"], emp["last_name"],
emp["first_name"], emp["ssn_encrypted"],
emp["hire_date"], emp["pay_frequency"],
emp["annual_salary"], emp["fed_filing_status"],
emp["state_code"], emp["cost_center"],
emp["department_code"], emp["effective_date"]])
return output.getvalue()
Verify: Check payroll provider portal → Inbound Files → expected: file listed with status "Received"
Push eligibility-triggering events from HRIS to benefits platform. [src3]
const axios = require('axios'); // [email protected]
async function syncBenefitsEligibility(hrisEvent) {
const BENEFITS_API = 'https://api.benefitsplatform.com/v2/eligibility';
const payload = {
member_id: hrisEvent.benefits_member_id,
event_type: mapEventType(hrisEvent.type),
effective_date: hrisEvent.effective_date,
employee: {
first_name: hrisEvent.first_name,
last_name: hrisEvent.last_name,
employment_status: hrisEvent.status,
salary: hrisEvent.annual_salary,
hire_date: hrisEvent.hire_date
}
};
const response = await axios.post(BENEFITS_API, payload, {
headers: { 'Authorization': `Bearer ${process.env.BENEFITS_API_TOKEN}` },
timeout: 30000
});
return response.data;
}
Verify: Benefits platform → Employee → Enrollment Status → expected: "Eligible"
After payroll runs, post journal entries to the ERP financial system. [src1]
def parse_payroll_results(csv_path):
journal_entries = []
with open(csv_path, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
journal_entries.append({
"account": f"6100-{row['cost_center']}",
"debit": float(row["gross_pay"]),
"credit": 0,
"reference": row["payroll_batch_id"]
})
journal_entries.append({
"account": "2100",
"debit": 0,
"credit": float(row["net_pay"]),
"reference": row["payroll_batch_id"]
})
return journal_entries
Verify: ERP GL → Journal Entries → expected: balanced debits = credits for pay period
| Source Field (HRIS) | Target Field (Payroll) | Type | Transform | Gotcha |
|---|---|---|---|---|
| Employee_ID (UUID) | EMP_ID (numeric) | String→Int | Xref table lookup | Never derive — must be stable bidirectional |
| Legal_Name.Last | LAST_NAME | String | Direct | Max 30 chars in most payroll vs 100+ in HRIS |
| Compensation.Annual_Salary | SALARY_ANNUAL | Decimal | Direct (same currency) | Multi-currency: convert at payroll-period rate |
| Work_Address.State | STATE_CODE | String | Map to 2-letter code | HRIS may store full state name |
| Pay_Frequency | PAY_FREQ_CODE | String | Map: Biweekly→B, Monthly→M | HRIS text vs payroll single-char code |
| Benefits.Medical_Plan | MEDICAL_DEDUCTION | Decimal | Lookup deduction by plan code | Plan code does not equal deduction amount |
| Tax_Elections.Federal_Filing | FED_STATUS | String | Map: Single→S, Married→M | W-4 effective date: retroactive vs prospective |
| Hire_Date | ORIG_HIRE_DATE | Date | Direct | Rehires: distinguish original vs most recent |
| Termination_Date | TERM_DATE | Date | Direct | Must arrive BEFORE last payroll run |
| Cost_Center | COST_CENTER | String | Direct or xref | Hierarchical (1000.2000) vs flat (123456) |
| Code / Error | Platform | Meaning | Resolution |
|---|---|---|---|
| HTTP 429 | Workday REST | Rate limit exceeded | Exponential backoff; check Retry-After header |
| HTTP 401 | All platforms | Authentication failure | Refresh token; check clock sync (SAML) |
| INVALID_EMPLOYEE_ID | Payroll | Employee not found | Check xref table; verify employee created in payroll first |
| CUTOFF_EXCEEDED | Payroll | Change after cut-off | Defer to next cycle; alert HR |
| DUPLICATE_RECORD | Benefits | Employee already enrolled | Implement idempotency key on events |
| FILE_DECRYPT_FAIL | Payroll SFTP | PGP decryption failed | Rotate keys; verify fingerprint |
| GL_IMBALANCE | ERP | Debits != credits | Reconcile totals before posting; add rounding line |
stagger schedules by 30-60 minutes; use dedicated integration users per system. [src2]enforce retro change window limit (max 2 prior periods) in middleware. [src3]monitor key expiry with 30-day alerting; maintain backup rotation process. [src3]switch to bulk mode; pre-scale iPaaS workers; dedicated batch windows. [src3]daily reconciliation comparing HRIS active vs IdP active users. [src1]convert to payroll system's local timezone before transmitting. [src2]// BAD — N systems create N*(N-1)/2 integration points
// HRIS <-> Payroll (custom API)
// HRIS <-> Benefits (custom SFTP)
// HRIS <-> ERP (custom file import)
// Payroll <-> ERP (custom journal posting)
// Result: 10+ custom integrations, no central error handling
// GOOD — iPaaS as central hub, N spoke connections
// HRIS → iPaaS (single outbound, event-driven)
// iPaaS → Payroll (transform + route)
// iPaaS → Benefits (transform + route)
// iPaaS → ERP (transform + route)
// Result: N integrations, centralized logging, single error queue
# BAD — pushing every comp change to payroll in real time
def on_comp_change(event):
payroll_api.update_salary(event.employee_id, event.new_salary)
# Problem: 50 changes/day = 50 API calls, payroll recalculates each time
# GOOD — collect all changes, send as single batch before cut-off
def prepare_payroll_batch(pay_period_end, cutoff_date):
changes = hris_api.get_changes(since=last_batch_date, until=cutoff_date)
validated = [c for c in changes if validate_for_payroll(c)]
payroll_api.submit_batch(pay_period_end, validated)
# Result: one atomic submission, all-or-nothing, easy to retry
# BAD — hardcoding US-only ACA eligibility
def check_eligibility(employee):
if employee.hours_per_week >= 30:
return "eligible"
# Breaks for: non-US, state mandates, union contracts
# GOOD — jurisdiction-based eligibility engine
def check_eligibility(employee, jurisdiction_rules):
rules = jurisdiction_rules.get(employee.work_country, {})
.get(employee.work_state, {})
for rule in rules.get("medical_eligibility", []):
if not rule.evaluate(employee):
return {"eligible": False, "reason": rule.failure_reason}
return {"eligible": True}
Define ID strategy and build xref table as FIRST task. [src5]Version-pin format; subscribe to provider release notes; test in sandbox. [src3]Use anonymized production data snapshots. [src7]Record-level error handling; quarantine invalid; alert HR. [src3]Build carrier feed SLAs into enrollment calendar. [src3]Audit logging at every step; hash payloads; never log PII in cleartext. [src1]# Test Workday RaaS connectivity and auth
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $WD_TOKEN" \
"https://wd5-services1.myworkday.com/$TENANT/customreport2/ISU_User/Test_Report?format=json"
# Expected: 200
# Verify SAP SuccessFactors OData access
curl -s -X GET \
"https://api.successfactors.com/odata/v2/User?\$top=1&\$format=json" \
-H "Authorization: Bearer $SF_TOKEN"
# Expected: JSON with single user record
# Check payroll SFTP connectivity
sftp -o BatchMode=yes -i /path/to/key [email protected] <<< "ls /inbound/"
# Expected: directory listing
# Validate PGP key is not expired
gpg --list-keys [email protected] | grep "expires:"
# Expected: expiry date in the future
# Count pending xref mappings
psql $MIDDLEWARE_DB -c "SELECT COUNT(*) FROM employee_xref WHERE payroll_employee_id IS NULL;"
# Expected: 0
| Platform | Current Version | Previous Stable | Key Changes | Migration Notes |
|---|---|---|---|---|
| Workday | 2025R2 | 2025R1 | SOAP-only Worker endpoints deprecated; REST Worker v2 GA | Migrate SOAP Worker integrations to REST; 12-month sunset |
| SAP SuccessFactors | 2H 2025 | 1H 2025 | SAP Integration Suite mandatory for EC Payroll; SFAPI deprecated | Use SAP BTP Integration Suite for new integrations |
| Oracle HCM Cloud | 24D | 24C | New Hire event REST API enhancements; FBDI v3 | FBDI v2 supported until 25C; test v3 templates |
| D365 HR | 10.0.40 | 10.0.39 | Payroll integration API v2 with batch support | v1 still supported; v2 adds idempotency keys |
| ADP Workforce Now | 2025.3 | 2025.2 | New webhooks for worker lifecycle events | Webhooks in GA; replaces polling pattern |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Running separate HRIS, payroll, and benefits systems | Single all-in-one HCM suite (Workday + Workday Payroll) | Vendor native configuration guide |
| 500+ employees with compliance risk from manual entry | < 50 employees with simple payroll | Manual process or spreadsheet onboarding |
| Multiple countries/jurisdictions | Single country, single provider, no benefits complexity | Simple HRIS-payroll file export |
| Need SOX-compliant auditable data trail | Non-regulated with minimal audit requirements | Direct file transfer without middleware |
| Post-acquisition with different HR systems | Greenfield where you can choose one platform | Single-vendor HCM selection |
| Capability | Workday HCM | SAP SuccessFactors | Oracle HCM Cloud | D365 HR |
|---|---|---|---|---|
| Primary API | REST + RaaS | OData v2 | REST + FBDI | OData v4 (Dataverse) |
| Real-time events | Business Process notifications | Intelligent Services | Business Events | Dataverse webhooks |
| Payroll integration | Payroll Connect API or native | Payroll Control Center or native | Global Payroll Interface or native | Payroll Integration API v2 |
| Benefits integration | Native Benefits module | Benefits Management module | Benefits module | Power Platform |
| iPaaS preference | Workday Studio, MuleSoft, Boomi | SAP Integration Suite (mandatory for EC Payroll) | Oracle OIC, MuleSoft | Power Automate, Azure Logic Apps |
| Rate limits | 60/min REST, 1K/min SOAP | 200 concurrent | Configurable per tenant | 6K per 5 min |
| Batch import | EIB | Import/Export templates | FBDI | Data Management Framework |
| Auth model | OAuth 2.0, ISU | OAuth 2.0 + SAML | OAuth 2.0 | OAuth 2.0 (Azure AD) |
| Sandbox quality | Full copy available | Limited test instance | Full sandbox + preview | Sandbox environments |
| Global payroll partners | 75+ | 50+ | 60+ | ISV partners |
| Change data capture | Prism Analytics | EC Change API, Audit Log | Business Events + CDC | Dataverse Change Tracking |