Skip to main content
CMDashCybersecurity Maturity Dashboard
← Back to Gusto

Coming soon

Gusto — Integration Setup

Connecting Gusto gives CMDash your employee and contractor roster: who works here, when they joined, when they left, and who reports to whom. That roster is the evidence behind joiner/leaver controls, periodic access reviews, and the contractor population auditors ask about separately.

Prerequisites

  • A Gusto account, with the connecting user holding a payroll admin role
  • CMDash Essentials tier or above

Gusto is the one HRIS in CMDash with a real OAuth handshake — there is no API key to paste. You authorize CMDash from Gusto's consent screen and Gusto issues a short-lived token that CMDash refreshes automatically.

Step 1: Connect in CMDash

  1. Navigate to Integrations in your CMDash dashboard.
  2. Find Gusto and click Connect.
  3. You are redirected to Gusto to sign in and approve the connection. CMDash requests exactly two scopes:
    • companies:read
    • employees:read
  4. Approve, and you are returned to CMDash.

Finish in the same browser within 10 minutes: the handshake is bound to the browser that clicked Connect by a short-lived Secure cookie, and a consent completed anywhere else is refused. That cookie needs CMDash served over HTTPS (browsers make an exception only for http://localhost), with its API on the app's own host (the /api path); a self-hosted install whose callback URL is plain HTTP is refused when Connect is clicked, with the reason.

CMDash does not request compensations:read. That is deliberate and load-bearing: without it, the pay-rate fields on Gusto's employee payload are unreadable by CMDash's token at all. The privacy promise below is enforced by Gusto, not merely by CMDash's code.

Optional connection settings

SettingPurpose
Company UUIDsRestrict the sync to specific companies. When unset, CMDash calls GET /v1/me and syncs every company your payroll-admin role can see.
API VersionPins a specific X-Gusto-API-Version. When unset, the header is not sent and Gusto's default applies.
Base URLPoint at Gusto's demo environment (https://api.gusto-demo.com) for testing.

What Syncs

Data TypeGusto APISync Frequency
Company discoveryGET /v1/meEach sync (skipped when Company UUIDs are pinned)
EmployeesGET /v1/companies/{uuid}/employees?page&per=100Every 4 hours by default (configurable per connection)
ContractorsGET /v1/companies/{uuid}/contractors?page&per=100Every 4 hours by default (configurable per connection)

Gusto keeps contractors in a separate collection from employees, so CMDash reads both and merges them into one roster with employmentType distinguishing them.

Each roster row is normalized to: external id, work email, display name, employment status, employment type, hire date, termination date, department, job title, and a manager reference. CMDash also derives a summary alongside the rows — headcount, active/terminated counts, contractor count, joiners and leavers in the last 30 days, and terminatedButActiveCount.

Status and date mapping

Gusto exposes three overlapping signals — terminated, onboarded, and current_employment_status — and they disagree in normal operation (a scheduled future termination sets terminated: true while the person is still working). CMDash resolves them in this order:

  1. terminated === true → terminated (Gusto's own flag wins)
  2. onboarded === false → pending (hired, not yet started or onboarding incomplete)
  3. an employment status present → active
  4. otherwise → unknown, not active

Hire date is the earliest hire_date across the employee's jobs[], not the primary job's. Gusto models an internal promotion as a new job with a later hire date; the earliest one is the "joined the company" date an auditor means.

Termination date comes from the active entry in terminations[].

What CMDash Deliberately Does Not Read

Gusto is a payroll product; its employee object is the most sensitive of the four HRIS integrations. Two protections apply.

First — never ask. Gusto gates its most sensitive fields behind explicit include opt-ins. CMDash sends none of them:

  • include=all_compensations — pay rates and full compensation history
  • include=all_home_addresses — home addresses
  • include=custom_fields — arbitrary tenant-defined fields of unknown sensitivity

Second — drop at the boundary. Gusto returns date_of_birth, ssn, has_ssn, phone, garnishments[], eligible_paid_time_off[], and jobs[].rate inline on the default payload, whether or not anyone wants them. CMDash constructs each roster record field by field from a fixed list and never copies a Gusto object wholesale, so these are discarded in memory at the end of the sync and never written to storage. Personal email is likewise never preferred over work_email.

Turning the roster into a control

Create a continuous-compliance rule against the roster:

  • Provider: Gusto
  • Data type: Workforce
  • Condition: terminatedButActiveCount <= 0
  • Target status: Implemented

The control reads as Implemented while every departed worker is correctly terminated in Gusto, and drifts to At Risk / Failing when one is not — with an auto-opened remediation task. Other useful fields on the same datum: headcount, activeCount, terminatedCount, contractorCount, joinersLast30d, leaversLast30d.

Training completion

Gusto has no training product, so training completion is not synced. Security-awareness training completion in CMDash comes from KnowBe4.

Troubleshooting

  • "Gusto token administers no companies": The connected user does not hold a payroll-admin role, or that role covers no companies. CMDash fails loudly here rather than reporting an empty roster, because an empty roster would silently clear every offboarding finding at once. Reconnect as a payroll admin, or pin Company UUIDs.
  • 401 Unauthorized: The refresh token was revoked (the connection was removed in Gusto). Reconnect.
  • Job title is blank: The employee has no jobs[] entry yet — common for someone still onboarding.
  • Contractor has no work email: Expected. A contractor with no company account genuinely has nothing to join to your identity provider, so the field is left empty rather than filled with a personal address.

Rate Limits & Sync Behavior

SettingValue
Sync frequencyEvery 4 hours by default (syncInterval is configurable per connection)
Page size100 records per request; the walk uses Gusto's X-Total-Pages header when present and stops on a short page otherwise
Page cap500 pages per collection (50,000 rows)
Rate limit handlingRequests are paced through CMDash's shared per-tenant token bucket
Circuit breakerRepeated failures open the breaker and skip the cycle rather than hammering Gusto
Failed sync retryJob queue retries 3 times with exponential backoff (starting at 30s) — see docs/developers/dead-letter.md

Credential Rotation

Gusto access tokens are short-lived and refreshed automatically by CMDash; there is nothing to rotate manually. To revoke access entirely, remove the CMDash authorization in Gusto (Settings → Integrations), then delete the connection in CMDash.

Deployment note

The OAuth client id and secret are deployment-level configuration (GUSTO_CLIENT_ID, GUSTO_CLIENT_SECRET, and optionally GUSTO_BASE_URL for the demo environment), registered once against your Gusto developer application — not per tenant.

Ready to see your posture?

Start a 30-day free trial of any paid plan — a card is required, and it converts unless you cancel — or sign up for Free with no card at all.