Skip to main content

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:

ProviderFeaturesSync TypeAPI
ADPTime, Tips, EarningsReal-timeREST API
GustoTime, Tips, EmployeesReal-timeREST API
PaychexTime, Tips, BenefitsBatchREST API
Custom ExportCSV, ExcelManualN/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 CategoryADP CodeGusto NamePaychex Code
Regular HoursREGRegular HoursRG
OvertimeOTOvertimeOT
Double TimeDTDouble TimeDT
Cash TipsTIPSTipsTIPS
Credit Card TipsCCTIPSCredit Card TipsCCTIPS
Sick PaySICKSickSK
VacationVACVacationVC
HolidayHOLHolidayHL
Break PayBRKBreak PayBK

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 CodeDescriptionResolution
EMPLOYEE_NOT_FOUNDEmployee missing in payroll systemSync employees first
INVALID_EARNING_CODEEarning code not configuredUpdate mapping
PERIOD_LOCKEDPay period already processedContact payroll admin
AUTH_EXPIREDOAuth token expiredReconnect integration
RATE_LIMITEDToo many requestsImplement 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

EventDescription
payroll.processedPayroll was successfully processed
payroll.failedPayroll processing failed
employee.createdNew employee added in payroll
employee.terminatedEmployee 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(())
}