Gusto Sensor
What it ingests
Org chart from Gusto App Integrations — the HRIS objects, not payroll:
| Gusto object | Ontology entity | Signal |
|---|---|---|
| Company | Organization | company |
| Department | Organization | department |
| Employee | Person (identity) | employee |
| Contractor | Person (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:
{
"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.
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
| Key | Required | Notes |
|---|---|---|
enabled_objects | no | Comma-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
- In the Gusto Developer Portal, create an App Integrations application.
- Register a redirect URI. Copy Client ID and Client Secret.
- 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.
- Store
client_id,client_secret,refresh_token,redirect_uri, andenvironment(productionordemo) as onegusto_api_clientvault row.
Wire-up
POST /api/sensors
{
"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_eventsdedupes ongusto:<signal>:<uuid>. - Stamps
OccurredAtfromupdated_atwhen 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.