Internal Build Documentation

Sperry Jobber Integration — Friction Report & Build

Diagnostic report on the Jobber API integration that powers the Sperry Tree Care campaign performance chart, including the scope-grant friction encountered and the working architecture deployed.

Prepared by BOSSTORQUE · Apr 24, 2026 · Status: Production

Executive Summary

The daily Sperry campaign chart was undercounting weekly Requests by roughly 10x — reporting 11 inbound requests for the week of Apr 19 when the actual count was 119. Root cause: the production script was counting newly-created Jobber Clients flagged as isLead=true, which only catches first-time customers. Repeat customers submitting new Requests (the majority of inbound volume) were invisible to the metric.

The straightforward fix — swap to Jobber's top-level Query.requests field — was blocked by Jobber's OAuth scope-grant logic. Despite the read_requests scope being toggled ON and saved in the developer center, every access token grant silently stripped it from the resulting permissions. After three full disconnect/reconnect cycles and a Jobber app config save-and-reauth, the scope remained absent from token grants while other newly-toggled scopes (Marketing, Custom Field Configurations, Users) came through correctly.

Workaround deployed: query Requests through the nested Client.requests connection. This path is gated only by read_clients (which we have) and returns the same Request data. The corrected daily script and a 26-week historical backfill are now live; the chart for week ending Apr 19 reads 119, matching ground truth.

11
Old Metric (this week)
119
New Metric (this week)
10.8x
Undercount Factor
26 wks
Backfilled History

1. The Friction

1.1 Original Defect

The production script sperry_requests_update.py ran daily at 06:03 AM, querying Jobber's GraphQL clients connection and counting clients with isLead=true created in the current week. This metric was named "Requests" in the chart but measured something different — first-time customer creations, not actual Request submissions.

For a tree care business with high repeat-customer volume, the gap between "new lead clients" and "Request submissions" is substantial. Most weekly inbound activity comes from existing clients re-engaging — those Requests never trigger an isLead flag because the client record already exists.

1.2 Friction Map — Time Spent vs. Path

PhaseActionOutcome
DiagnoseCompared script output (11) to known correct value (119) — identified isLead vs Request mismatchWin
ProbeTested top-level Query.requests with existing access tokenBlocked: permissions
Scope checkInspected access token JWT — only read_clients read_quotes read_jobs read_invoices granted despite Requests scope toggled ONDiscovery
Reauth #1Disconnected via appDisconnect mutation, re-ran OAuth flowSame 4 scopes
Reauth #2Added explicit scope= param to auth URLParam ignored — Jobber doesn't accept it
App saveSuspected uncommitted scope changes — clicked Save App in dev centerUnlocked: marketing, custom_field_configurations, users, expenses
Reauth #3Fresh consent flow with all scopes saved + clean app configread_requests still silently dropped
PivotSchema introspection on Client type — found Client.requests connectionWin
VerifyTested nested query under read_clients scope aloneReturns Request data
DeployPatched script, ran live — 118 this week, 60 prior week (within 1 of 119 target)Production

1.3 Jobber Platform Issues Uncovered

Silent scope dropping. The Jobber developer center allows toggling scopes ON in DRAFT mode, displays them as enabled, and saves the configuration successfully — but the OAuth grant logic strips specific scopes (notably read_requests, read_scheduled_items, and most write_* scopes) from access tokens without any error, warning, or indication. There is no documented mechanism to enable these scopes in DRAFT mode short of submitting the app for marketplace review.

Missing OAuth parameters. Jobber's OAuth implementation does not accept the standard scope= URL parameter. Scopes are derived solely from app configuration. Standard OAuth tooling and libraries that pass scopes via URL will appear to work but produce silently underscoped tokens.

Misleading dev center UX. The "Request a review" button leads to a Google Form with extensive marketplace publication requirements (logo, ToS URL, Privacy Policy URL, end-to-end testing checklist). Per Jobber's own Custom Integrations documentation, apps connecting to fewer than 5 paying accounts do not require review and can operate in DRAFT permanently — but the UI strongly implies review is the path forward when scopes don't grant correctly.

2. The Working Architecture

2.1 Data Flow

ComponentRole
sperry_requests_update.pyDaily updater. Auths to Jobber, queries weekly Request counts, patches CF Worker, redeploys.
run_secrets.jsonStores rotating Jobber refresh token + client credentials. Token is rotated by Jobber on every refresh; script saves new token immediately to disk before any further work.
Jobber GraphQL APISource of truth. Queried via Client.requests nested connection (read_clients scope only).
CF Worker sperry-requests-apr2026Renders the chart. rawCounts[] array drives YoY table, stat cards, EV panel — all via JS, never hardcoded HTML.
Scheduled Task sperry-requests-daily-updateCron 0 6 * * *. Runs the script daily at 06:03 AM Pacific. Built-in zero-guard aborts deploy if both counts return 0.

2.2 The Workaround Query

Jobber's OAuth scope check applies at the field level. The top-level Query.requests field requires read_requests. The nested Client.requests connection requires only read_clients. Since Client records bump their updatedAt timestamp whenever a new Request is submitted on their behalf, we can filter clients to those touched in the last 2 weeks, then iterate their nested Requests and bucket by week.

query Clients($after: String, $filter: ClientFilterAttributes) {
  clients(
    first: 30,
    after: $after,
    filter: $filter,
    sort: { key: UPDATED_AT, direction: DESCENDING }
  ) {
    pageInfo { hasNextPage endCursor }
    nodes {
      id
      updatedAt
      requests(first: 50) {
        nodes { id createdAt }
      }
    }
  }
}

Filter: { updatedAt: { after: prior_sunday } }. Returns approximately 280 active clients per run, well under the throttle budget. Total query cost ~150 units against a 10,000 budget.

2.3 Verification

Week IndexDate RangeOld MethodNew MethodDelta
328Apr 19 – Apr 2511119+108
327Apr 12 – Apr 181060+50
326Apr 5 – Apr 1177consistent
322Mar 8 – Mar 1497consistent

Indices 302–326 were already populated with correct historical Request data from prior runs of a different methodology — only [327] and [328] (last 2 weeks) had been overwritten with the broken isLead-based count.

3. Operational Runbook

3.1 Daily Run

The scheduled task sperry-requests-daily-update runs at 06:03 AM Pacific. It:

  1. Refreshes the Jobber access token (rotates the refresh token, saves new value immediately to run_secrets.json).
  2. Computes this week (Sun → now) and prior week (prior Sun → this Sun) ranges in UTC.
  3. Runs the Client.requests nested query with safety stop at 50 pages.
  4. Fetches the CF Worker's current code, parses rawCounts[], patches indices [this_idx] and [prior_idx].
  5. Updates the hardcoded Apr 19 storm-table entry to match the new this_idx value.
  6. Aborts via RuntimeError if both counts are 0 (prevents wiping the chart on a data-pull failure).
  7. Redeploys the worker via Cloudflare API.

3.2 Manual Run

python3 "/Users/Jason/My Drive (jason@bosstorque.ai)/1_Clients/Sperry Tree Care/2. Campaign Performance/scripts/sperry_requests_update.py"

3.3 Refresh Token Recovery

If the saved refresh token becomes invalid (HTTP 401 on token refresh), the connection must be re-authorized:

  1. Visit the OAuth consent URL in a logged-in Jobber browser tab:
    https://api.getjobber.com/api/oauth/authorize?response_type=code&client_id=7e52aab6-cb92-4318-9261-beb1f4249447&redirect_uri=https://localhost/callback
  2. Click Allow Access on the consent screen.
  3. Browser will fail to load localhost — copy the code=... value from the URL bar.
  4. Exchange the code for a refresh token by POSTing to https://api.getjobber.com/api/oauth/token with grant_type=authorization_code, the code, client_id, client_secret, and the same redirect_uri.
  5. Update JOBBER_REFRESH_TOKEN in run_secrets.json.

3.4 Backfill

To re-baseline historical weeks under the new methodology, run sperry_requests_backfill.py with WEEKS_BACK set to the desired number of weeks. The 26-week (6-month) backfill executed on Apr 24, 2026 covers indices 302–328.

4. Open Issues & Risks

4.1 Pagination Limits

The query fetches the most recent 50 Requests per Client. For the rare repeat customer with more than 50 Requests in a 6-month window, older Requests would be missed. Sperry's customer pattern (1–3 Requests per client per year) makes this a non-issue in practice, but a high-volume contract account could expose it.

4.2 Scope-Grant Fragility

The current architecture depends on the Client.requests nested connection remaining accessible under read_clients alone. If Jobber tightens its scope enforcement to require read_requests for all Request data (including nested), this script breaks immediately and silently — the query would error and counts would zero out, triggering the zero-guard abort.

4.3 Recommended Follow-up

5. Reference

ItemValue
Jobber App NameBOSSTORQUE Sperry Report
StatusDRAFT (does not require review per Jobber custom integration policy)
Jobber AccountSperry Tree Care (account_id 1970331)
Client ID7e52aab6-cb92-4318-9261-beb1f4249447
Granted Scopesread_clients, read_quotes, read_jobs, read_invoices, read_jobber_payments, read_users, write_users, write_tax_rates, read_expenses, read_custom_field_configurations, write_custom_field_configurations, read_marketing, write_marketing
Silently Droppedread_requests, read_scheduled_items, write_clients, write_quotes, write_jobs, write_invoices
Chart URLsperry-requests-apr2026.jason-8ce.workers.dev
Terms of Service URLsperry-bosstorque-tos.jason-8ce.workers.dev
Privacy Policy URLsperrytreecare.com/privacy-policy