Payroll Integrations
Automate payroll processing with leading payroll providers.
Overview
Olympus Cloud integrates with major payroll platforms to export employee time records, tips, and labor data:
| Provider | Features | Sync Type | API |
|---|---|---|---|
| ADP | Time, Tips, Earnings | Real-time | REST API |
| Gusto | Time, Tips, Employees | Real-time | REST API |
| Paychex | Time, Tips, Benefits | Batch | REST API |
| Custom Export | CSV, Excel | Manual | N/A |
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Olympus Workforce │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Time Clock│ │ Tips │ │ Schedules│ │ Employees│ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼─────────────┼─────────────┼─────────────┼───────────────┘
│ │ │ │
└─────────────┴──────┬──────┴─────────────┘
│
┌──────────────▼───────────────┐
│ Payroll Integration Hub │
│ • Data normalization │
│ • Earning code mapping │
│ • Schedule validation │
└──────────────┬───────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌────▼────┐ ┌─────▼────┐ ┌────▼────┐
│ ADP │ │ Gusto │ │ Paychex │
└─────────┘ └──────────┘ └─────────┘
ADP Integration
Setup
# config/integrations/adp.yaml
adp:
client_id: "your-client-id"
client_secret: "your-client-secret"
environment: production # or sandbox
company_code: "ABC123"
org_unit_id: "your-org-unit"
webhook_url: "https://api.olympuscloud.ai/webhooks/adp"
OAuth Authentication
// src/integrations/adp/auth.rs
pub struct ADPAuth {
client_id: String,
client_secret: String,
base_url: String,
}
impl ADPAuth {
pub async fn get_access_token(&self) -> Result<String> {
let client = reqwest::Client::new();
let response = client
.post(&format!("{}/auth/oauth/v2/token", self.base_url))
.form(&[
("grant_type", "client_credentials"),
("client_id", &self.client_id),
("client_secret", &self.client_secret),
])
.send()
.await?;
let token_response: TokenResponse = response.json().await?;
Ok(token_response.access_token)
}
}
Export Time Records
// src/integrations/adp/time.rs
pub async fn export_time_records(
ctx: &TenantContext,
pay_period: DateRange,
) -> Result<ADPTimeExport> {
let token = adp_auth.get_access_token().await?;
let employees = employee_service.get_active(ctx).await?;
let mut time_cards = Vec::new();
for employee in employees {
// Get time records for this employee
let records = time_service.get_for_period(ctx, &employee.id, &pay_period).await?;
// Get tips for this employee
let tips = tip_service.get_for_period(ctx, &employee.id, &pay_period).await?;
let time_card = ADPTimeCard {
associate_id: employee.adp_id.clone(),
pay_period_start: pay_period.start.format("%Y-%m-%d").to_string(),
pay_period_end: pay_period.end.format("%Y-%m-%d").to_string(),
time_entries: records.into_iter().map(|r| ADPTimeEntry {
date: r.date.format("%Y-%m-%d").to_string(),
hours_worked: r.regular_hours,
overtime_hours: r.overtime_hours,
earning_code: map_earning_code(&r.job_code),
department_code: r.department_code,
position_code: r.position_code,
}).collect(),
earnings: vec![
ADPEarning {
earning_code: "TIPS",
amount: tips.total_tips,
},
ADPEarning {
earning_code: "CCTIPS",
amount: tips.credit_card_tips,
},
],
};
time_cards.push(time_card);
}
// Submit to ADP
let client = reqwest::Client::new();
let response = client
.post(&format!("{}/time/v2/workers/time-cards", ADP_BASE_URL))
.header("Authorization", format!("Bearer {}", token))
.json(&time_cards)
.send()
.await?;
Ok(response.json().await?)
}
Employee Sync
pub async fn sync_employees_from_adp(
ctx: &TenantContext,
) -> Result<SyncResult> {
let token = adp_auth.get_access_token().await?;
let client = reqwest::Client::new();
let response = client
.get(&format!("{}/hr/v2/workers", ADP_BASE_URL))
.header("Authorization", format!("Bearer {}", token))
.send()
.await?;
let adp_employees: Vec<ADPWorker> = response.json().await?;
let mut result = SyncResult::default();
for adp_emp in adp_employees {
let employee = Employee {
external_id: Some(adp_emp.associate_id.clone()),
first_name: adp_emp.person.legal_name.given_name,
last_name: adp_emp.person.legal_name.family_name,
email: adp_emp.person.email,
phone: adp_emp.person.phone,
department: adp_emp.work_assignment.department_code,
position: adp_emp.work_assignment.job_title,
hire_date: adp_emp.work_assignment.hire_date,
hourly_rate: adp_emp.work_assignment.pay_rate,
..Default::default()
};
employee_service.upsert(ctx, &employee).await?;
result.synced += 1;
}
Ok(result)
}
Gusto Integration
Setup
# config/integrations/gusto.yaml
gusto:
client_id: "your-client-id"
client_secret: "your-client-secret"
redirect_uri: "https://api.olympuscloud.ai/oauth/gusto/callback"
webhook_url: "https://api.olympuscloud.ai/webhooks/gusto"
OAuth Connection
// src/integrations/gusto/auth.rs
pub struct GustoAuth {
client_id: String,
client_secret: String,
redirect_uri: String,
}
impl GustoAuth {
pub fn get_authorization_url(&self, tenant_id: &str) -> String {
format!(
"https://api.gusto.com/oauth/authorize?\
client_id={}&\
redirect_uri={}&\
response_type=code&\
state={}",
self.client_id,
urlencoding::encode(&self.redirect_uri),
tenant_id
)
}
pub async fn exchange_code(&self, code: &str) -> Result<GustoTokens> {
let client = reqwest::Client::new();
let response = client
.post("https://api.gusto.com/oauth/token")
.form(&[
("client_id", &self.client_id),
("client_secret", &self.client_secret),
("code", code),
("redirect_uri", &self.redirect_uri),
("grant_type", "authorization_code"),
])
.send()
.await?;
response.json().await
}
}
Export Payroll Data
// src/integrations/gusto/payroll.rs
pub async fn create_payroll(
ctx: &TenantContext,
pay_period: DateRange,
) -> Result<GustoPayroll> {
let tokens = gusto_auth.get_tokens(ctx).await?;
let company_id = get_gusto_company_id(ctx).await?;
// Get all time records
let time_records = time_service.get_for_period_all(ctx, &pay_period).await?;
// Group by employee
let by_employee = time_records.into_iter()
.group_by(|r| r.employee_id.clone());
let mut employee_compensations = Vec::new();
for (employee_id, records) in &by_employee {
let employee = employee_service.get(ctx, &employee_id).await?;
let gusto_employee_id = employee.gusto_id
.ok_or(Error::MissingIntegrationId)?;
let records: Vec<_> = records.collect();
let regular_hours: f64 = records.iter().map(|r| r.regular_hours).sum();
let overtime_hours: f64 = records.iter().map(|r| r.overtime_hours).sum();
let tips = tip_service.get_total(ctx, &employee_id, &pay_period).await?;
employee_compensations.push(GustoEmployeeCompensation {
employee_uuid: gusto_employee_id,
fixed_compensations: vec![
FixedCompensation {
name: "Tips".into(),
amount: format!("{:.2}", tips),
job_uuid: None,
},
],
hourly_compensations: vec![
HourlyCompensation {
name: "Regular Hours".into(),
hours: format!("{:.2}", regular_hours),
job_uuid: None,
},
HourlyCompensation {
name: "Overtime".into(),
hours: format!("{:.2}", overtime_hours),
job_uuid: None,
},
],
});
}
// Create payroll
let client = reqwest::Client::new();
let response = client
.put(&format!(
"https://api.gusto.com/v1/companies/{}/payrolls/{}/{}",
company_id,
pay_period.start.format("%Y-%m-%d"),
pay_period.end.format("%Y-%m-%d")
))
.header("Authorization", format!("Bearer {}", tokens.access_token))
.json(&json!({
"version": "string",
"employee_compensations": employee_compensations
}))
.send()
.await?;
Ok(response.json().await?)
}
Paychex Integration
Setup
# config/integrations/paychex.yaml
paychex:
client_id: "your-client-id"
client_secret: "your-client-secret"
display_id: "your-company-display-id"
webhook_url: "https://api.olympuscloud.ai/webhooks/paychex"
Authentication
// src/integrations/paychex/auth.rs
pub async fn get_access_token(
client_id: &str,
client_secret: &str,
) -> Result<String> {
let client = reqwest::Client::new();
let response = client
.post("https://api.paychex.com/auth/oauth/v2/token")
.form(&[
("grant_type", "client_credentials"),
("client_id", client_id),
("client_secret", client_secret),
])
.send()
.await?;
let token: TokenResponse = response.json().await?;
Ok(token.access_token)
}
Time Import
pub async fn export_time_to_paychex(
ctx: &TenantContext,
pay_period: DateRange,
) -> Result<PaychexTimeImport> {
let token = get_access_token(&config).await?;
let company_id = get_paychex_company_id(ctx).await?;
let time_records = time_service.get_for_period_all(ctx, &pay_period).await?;
let punches: Vec<PaychexPunch> = time_records.iter().flat_map(|r| {
vec![
PaychexPunch {
worker_id: r.employee_external_id.clone(),
punch_type: "IN",
punch_datetime: r.clock_in.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
org_level_1: r.department_code.clone(),
org_level_2: r.location_code.clone(),
},
PaychexPunch {
worker_id: r.employee_external_id.clone(),
punch_type: "OUT",
punch_datetime: r.clock_out.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
org_level_1: r.department_code.clone(),
org_level_2: r.location_code.clone(),
},
]
}).collect();
let client = reqwest::Client::new();
let response = client
.post(&format!(
"https://api.paychex.com/companies/{}/time/punches",
company_id
))
.header("Authorization", format!("Bearer {}", token))
.json(&json!({ "punches": punches }))
.send()
.await?;
Ok(response.json().await?)
}
Earning Code Mapping
Configuration
| Olympus Category | ADP Code | Gusto Name | Paychex Code |
|---|---|---|---|
| Regular Hours | REG | Regular Hours | RG |
| Overtime | OT | Overtime | OT |
| Double Time | DT | Double Time | DT |
| Cash Tips | TIPS | Tips | TIPS |
| Credit Card Tips | CCTIPS | Credit Card Tips | CCTIPS |
| Sick Pay | SICK | Sick | SK |
| Vacation | VAC | Vacation | VC |
| Holiday | HOL | Holiday | HL |
| Break Pay | BRK | Break Pay | BK |
Mapping Function
pub fn map_earning_code(
platform: PayrollPlatform,
olympus_code: &str,
) -> String {
let mapping = match platform {
PayrollPlatform::ADP => hashmap! {
"REGULAR" => "REG",
"OVERTIME" => "OT",
"TIPS" => "TIPS",
"SICK" => "SICK",
},
PayrollPlatform::Gusto => hashmap! {
"REGULAR" => "Regular Hours",
"OVERTIME" => "Overtime",
"TIPS" => "Tips",
"SICK" => "Sick",
},
PayrollPlatform::Paychex => hashmap! {
"REGULAR" => "RG",
"OVERTIME" => "OT",
"TIPS" => "TIPS",
"SICK" => "SK",
},
};
mapping.get(olympus_code)
.map(|s| s.to_string())
.unwrap_or_else(|| olympus_code.to_string())
}
Sync Schedule
Automatic Export
# config/payroll_sync.yaml
payroll:
export_schedule:
# Export time data daily at 2 AM
daily: "0 2 * * *"
# Full sync on pay period end
pay_period_end: true
options:
include_tips: true
include_breaks: true
round_to_nearest: 15 # minutes
overtime_threshold: 40 # hours per week
split_by_department: true
Manual Export
POST /api/v1/integrations/payroll/export
Authorization: Bearer {access_token}
Content-Type: application/json
Request:
{
"platform": "adp",
"pay_period_start": "2026-01-01",
"pay_period_end": "2026-01-15",
"options": {
"include_tips": true,
"include_breaks": false
}
}
Response:
{
"export_id": "exp_abc123",
"status": "completed",
"employees_exported": 25,
"total_hours": 2500.5,
"total_tips": 4250.00,
"errors": []
}
Error Handling
Common Errors
| Error Code | Description | Resolution |
|---|---|---|
EMPLOYEE_NOT_FOUND | Employee missing in payroll system | Sync employees first |
INVALID_EARNING_CODE | Earning code not configured | Update mapping |
PERIOD_LOCKED | Pay period already processed | Contact payroll admin |
AUTH_EXPIRED | OAuth token expired | Reconnect integration |
RATE_LIMITED | Too many requests | Implement backoff |
Retry Logic
pub async fn export_with_retry(
ctx: &TenantContext,
platform: PayrollPlatform,
pay_period: DateRange,
) -> Result<ExportResult> {
let max_retries = 3;
let mut attempt = 0;
loop {
match export_time_records(ctx, platform, &pay_period).await {
Ok(result) => return Ok(result),
Err(e) if e.is_retryable() && attempt < max_retries => {
attempt += 1;
let delay = Duration::from_secs(2u64.pow(attempt));
tokio::time::sleep(delay).await;
}
Err(e) => return Err(e),
}
}
}
Webhooks
Supported Events
| Event | Description |
|---|---|
payroll.processed | Payroll was successfully processed |
payroll.failed | Payroll processing failed |
employee.created | New employee added in payroll |
employee.terminated | Employee terminated in payroll |
Webhook Handler
pub async fn handle_payroll_webhook(
platform: PayrollPlatform,
event: WebhookEvent,
) -> Result<()> {
match event.event_type.as_str() {
"payroll.processed" => {
// Update payroll status
payroll_service.mark_processed(&event.payroll_id).await?;
}
"employee.created" => {
// Sync new employee
let employee = employee_service.fetch_from_payroll(
platform,
&event.employee_id
).await?;
employee_service.create(&employee).await?;
}
_ => {}
}
Ok(())
}
Related Documentation
- Workforce API - Time and attendance
- Employees API - Employee management
- Accounting Integration - Financial sync