Skip to content

Gusto Sensor

What it ingests

Org chart from Gusto App Integrations — the HRIS objects, not payroll:

Gusto objectOntology entitySignal
CompanyOrganizationcompany
DepartmentOrganizationdepartment
EmployeePerson (identity)employee
ContractorPerson (identity)contractor

A department is an org unit (the HubSpot company analog); an employee or contractor is identity — they resolve to Person via work email / name, the same contract as a HubSpot contact or Rippling worker. Employees MEMBER_OF their department. Manager is identity only (no Person→Person edge).

This is Gusto alone. Workday, BambooHR, and Rippling are sibling sources with different APIs. Greenhouse is the ATS sibling — do not configure this source against Greenhouse.

Read-only and poll-based against https://api.gusto.com (or https://api.gusto-demo.com for demo). OAuth 3LO browser install and Gusto webhooks are out of v1.

Compensation, SSN, bank accounts, date of birth, home address, and EIN are never requested.

Credential

References a gusto_api_client credential by id. The secret is a JSON blob:

json
{
  "client_id": "<Developer Portal app id>",
  "client_secret": "<Developer Portal app secret>",
  "refresh_token": "<company-level refresh token>",
  "redirect_uri": "https://your-app.example/callback",
  "environment": "production"
}

environment is production (default, api.gusto.com) or demo (api.gusto-demo.com). There is no free-form host. redirect_uri must match the callback registered on the Gusto app — Gusto requires it on every token refresh.

Auth is the OAuth 2.0 refresh-token grant. Complete the company OAuth install in the Gusto Developer Portal, then paste the company-level refresh token. System access tokens (Oauth::Application) cannot read employees and are refused at Test connection.

Gusto refresh tokens are single-use. Corveil writes the rotated token back to the same vault row after mint, including after a failed Test connection, so the next poll still authenticates.

bash
curl -sS -X POST https://your-corveil-host/api/credentials \
  -H "Authorization: Bearer <admin>" -H "Content-Type: application/json" \
  -d '{"type":"gusto_api_client","name":"acme-gusto","secret":"{\"client_id\":\"…\",\"client_secret\":\"…\",\"refresh_token\":\"…\",\"redirect_uri\":\"https://example.com/callback\"}"}'

Config fields

KeyRequiredNotes
enabled_objectsnoComma-separated Gusto object kinds. Blank polls every supported kind. Unknown names are rejected at configure time.

Supported object names: companies, departments, employees, contractors. Singular aliases (company, employee, worker) are accepted.

There is no default_entity_type. This source emits Organization across two signals plus Person via identity, so a single override would silently mistype one of them. Company UUID is discovered from the token (GET /v1/token_info), not a config field.

Provider-side setup

  1. In the Gusto Developer Portal, create an App Integrations application.
  2. Register a redirect URI. Copy Client ID and Client Secret.
  3. Complete the OAuth install against the company you want to ingest (demo or production). Copy the company-level refresh token — not a system access token.
  4. Store client_id, client_secret, refresh_token, redirect_uri, and environment (production or demo) as one gusto_api_client vault row.

Wire-up

POST /api/sensors

json
{
  "sensor_type": "gusto",
  "name": "Acme Gusto",
  "config": {
    "enabled_objects": "companies, departments, employees, contractors"
  },
  "credentials": { "api_client": "<gusto_api_client credential id>" }
}

Verify

Run Test connection. It mints an access token, confirms the token is company-scoped, and authenticates against the first enabled object's list endpoint, so a 401/403 fails at save/test rather than on the first poll.

Once polling, each object kind's count appears in the per-poll summary log (companies_collected, employees_collected, …). A kind stuck at zero while the others move is the signal to check that kind's Gusto permission.

How incremental sync works

Gusto collections paginate with page / per and X-Total-Pages. They do not expose an updated_after filter comparable to Harvest. Corveil:

  • Walks each collection from the persisted page, up to 10 pages (1,000 records) per object per poll.
  • Resets the page to 1 when a collection drains, so the next poll rescans from the head. sensor_events dedupes on gusto:<signal>:<uuid>.
  • Stamps OccurredAt from updated_at when present; otherwise uses ingest time (ingest_fallback).
  • Access tokens last about two hours; a 401 re-mints once from the stored (and possibly rotated) refresh token.
  • Pins X-Gusto-API-Version: 2024-04-01.

Deletes are invisible. An employee vanishing from results is not treated as a deletion.

Not included

  • Workday / BambooHR / Rippling. Different HRIS products; do not configure this source against them.
  • Payroll, compensation, SSN, bank, DOB, EIN. Never requested (include= is never sent).
  • OAuth authorization-code (browser). The operator completes install in the Gusto Developer Portal and pastes the refresh token.
  • Webhooks. Out of v1; polling is the supported path.