Skip to main content

Gusto HR Source

nexusID integrates with Gusto as an authoritative HR source and drives the full joiner/mover/leaver (JML) lifecycle from it. Gusto is API-first (OAuth 2.0), so the integration pulls the employee directory directly from the Gusto API — no file transfer.

How it flows

Gusto API (OAuth2)
→ GustoProvider.fetchRecords() (integration/hr-gusto)
→ GustoConnector.upsert() (match by employeeId = Gusto uuid)
→ LifecycleEventService onJoiner / onMover / onLeaver
→ LifecycleScheduleExecutor (PROVISION → ENABLE → DISABLE)
→ Entra / AD / SCIM / downstream provisioning + deprovisioning
→ HR source precedence + WorkflowEngine (JOINER / MOVER / LEAVER)

Identical pipeline to the Paycom HR source — only the external connector differs.

Add Gusto as an HR_SOURCE integration in Admin → Integrations — no environment variables, no redeploy. Pick the Gusto provider and fill in: accessToken (OAuth2 Bearer), companyId (company UUID), baseUrl (default https://api.gusto.com), and apiVersion. Secrets are encrypted at rest, and the unified HR sync sweep picks it up. See HR Sources for the model.

Gusto — Add HR source connection fields

Legacy: environment-variable configuration

The GUSTO_SYNC_* variables below are an optional alternative for GitOps / air-gapped deployments that keep secrets out of the database. They're off by default — most operators should use the admin console above.

Inbound configuration (env, legacy)

GUSTO_SYNC_ENABLED=true
GUSTO_SYNC_ACCESS_TOKEN=... # OAuth 2.0 bearer access token
GUSTO_SYNC_COMPANY_ID=<company uuid>
GUSTO_SYNC_BASE_URL=https://api.gusto.com # https://api.gusto-demo.com for the demo env
GUSTO_SYNC_API_VERSION=2024-04-01 # sent as X-Gusto-API-Version
GUSTO_SYNC_CRON="0 0 0,6,12,18 * * *" # every 6h

nexusID calls GET /v1/companies/{companyId}/employees (paginated) with Authorization: Bearer <token> and the X-Gusto-API-Version header.

Field mapping

Each canonical field maps to a dot-path in a Gusto employee object (defaults are Gusto-accurate):

CanonicalDefault Gusto pathNotes
employeeId (required)uuidthe Gusto employee UUID (match key)
emailemail
firstName / lastNamefirst_name / last_namedisplay name is derived if absent
jobTitlejobs.0.titlenested — first job's title
departmentdepartment
statusterminatedbooleantrue = TERMINATED, false = ACTIVE

A leaver is detected when status resolves to TERMINATED/TERMED/INACTIVE. If your tenant exposes a string status instead, point status at current_employment_status (it's upper-cased).

Scheduling & near-real-time

GustoSyncScheduler polls on GUSTO_SYNC_CRON (default every 6h). Lower the interval for near-real-time change detection.

Multi-source precedence

When Gusto is one of several HR feeds, HR Source Precedence decides which source wins per attribute. Add GUSTO to the precedence order.

Outbound (nexusID → Gusto write-back)

nexusID can push termination on leaver back to Gusto via POST /v1/employees/{id}/terminations. It's off by default and never writes to Gusto unless explicitly enabled, best-effort (a failure never breaks the JML flow), and skips Gusto-originated terminations to avoid echo.

GUSTO_SYNC_OUTBOUND_ENABLED=true
GUSTO_SYNC_OUTBOUND_ACCESS_TOKEN=... # falls back to GUSTO_SYNC_ACCESS_TOKEN
GUSTO_SYNC_RUN_TERMINATION_PAYROLL=false

Attribute updates (mover) are not pushed to Gusto: its update endpoints require a per-record optimistic-lock version, so write-back is scoped to terminations.

Where it lives in code

  • Provider: modules/integration/hr-gusto/.../GustoProvider.kt (+ GustoApiClient.kt, GustoWriteClient.kt)
  • Connector + scheduler + outbound + config: modules/api/scim/.../connector/gusto/
  • Outbound dispatch: HrOutboundWriter invoked from LifecycleEventService