Skip to content

Workiz Integration

Workiz is an FSM (field-service management) platform aimed at small-to-mid-size service businesses (locksmiths, HVAC, appliance repair).

Everything JB360 does against the Workiz API. Workiz is platformId 2 in the onboarding wizard (ServiceTitan is 1); a dealer is a Workiz dealer when crmSystem = "Workiz" (tblDealerOnboard/tblDealer) and a row exists in tblWorkizAccounts. The integration lives entirely in jbw-sam (Node Lambdas) and jbwui (React) — the Java modules (extraction/, jbw-api/) contain zero Workiz code.

:::

  • API base: https://api.workiz.com/api/v1/
  • Auth: a single shared API token embedded in the URL path (…/api/v1/jb_warranties_7a9f…/) — no OAuth, no token exchange, no per-dealer credentials (hardcoded; see the Security & drift callout above).
  • Per-dealer scoping: the dealer’s Workiz Account ID (captured at onboarding, stored in tblWorkizAccounts) is sent as an HTTP header. ⚠ The header name is spelled inconsistently: Account-Id (jobs fetch, locations fetch) vs account_id (account verify).
  • No tenant concept: Workiz dealers reuse dealerId as tenantId everywhere (jbwdealeradmintoolsupdate/index.js:182-185, jbwworkizjobsdatasave/index.js:6-7).
  • Portal deep-links (UI): REACT_APP_WZ_URL — prod/dev https://app.workiz.com/root/job/, stage https://jbwarrantiestests.tests.workiz.com/root/job/.

All three calls are GET; there is no Workiz write-back.

EndpointUsed byPurpose
GET /job/all/?status=done&utc_updated={syncStartDate}&limit={n}&offset={n}jbwworkizjobsdatafetch (index.js:62-70)Incremental pull of completed jobs since last sync → invoices/line-items
GET /job/all/jbwworkizaccountverify (index.js:10-18)Onboarding: validate the dealer’s Account ID (response.code 200 → “Successful”, 401 → error msg)
GET /job/getLocationsWithLatestJobs/?limit={n}&offset={n}jbwagreementsdatamatch Workiz branch (index.js:435-447)Pull all job locations → tblWorkizLocationData for agreement/location matching

Pagination contract (jobs + locations): response carries flag (truthy = valid), data (array), has_more; the loop bumps offset by limit (default 50) until has_more !== true (jbwworkizjobsdatafetch/index.js:168-201, jbwagreementsdatamatch/index.js:511-544).

LambdaTriggerWhat it does
jbwworkizaccountverifyAPI GW GET /workiz/accountverify (template.yaml:1607)Validates an Account ID against Workiz during onboarding. No DB access.
jbwworkizjobsdatafetchSQS (reads event.Records[0].body) — ⚠ no event source declared in template.yaml (:1632-1654); wired out-of-bandPulls done jobs (paginated), maps them to invoice objects (parseWorkizJsonData), invokes the save Lambda synchronously via JBW_WORKIZ_JOBS_DATA_SAVE
jbwworkizjobsdatasaveLambda invoke only (template.yaml:1614-1631)Upserts tblInvoice + tblInvoiceItem (line items with Type==="equipment"isInstalledEquipment=1), updates tblAgreementEquipments (manufacturer/model), bumps tblTransmissionDetails (+History, destinationSystem='Workiz')
jbwagreementsdatamatchLambda invoke only (template.yaml:1100-1124)Shared ST/Workiz matcher. Workiz branch (crmSystem==="Workiz", index.js:19): repopulates tblWorkizLocationData (500-row batches), pulls JBW agreements → tblJBWAgreementData, then CALL jbw_st_dataSync(dealerId,'Workiz')
jbwstdatasyncspcallAPI GW GET /agreementsync/{id}/spcall (template.yaml:1148)On-demand re-sync; picks the Workiz path when the dealer row has an accountId (index.js:81-96)
jbwdealeradmintoolsupdatePUT /dealer/{id}/admtools — ⚠ route/function absent from template.yaml (deployed outside IaC)Admin tools (status/invoice/lastsyncdate/…); for Workiz lastsyncdate it invokes GET_WORKIZ_TENANT_INFO — see gaps below

The wizard is 4 steps for Workiz vs 5 for ServiceTitan — no client secret, no custom fields/tags:

  1. ClientPlatform.jsx:20 — pick Workiz (platformId 2) → routes to the Account ID step.
  2. ClientAccountId.jsx — dealer enters their Workiz Account ID; “Connect to Workiz” calls GET /workiz/accountverify?accountId=… and only proceeds on "Successful" (:92-98).
  3. CLientConnect.jsx — “Onboard” posts to POST /newdealeronboard.
  4. jbwnewonboarding/index.js — discriminates purely on accountId presence (platform = accountId ? "Workiz" : "ServiceTitan", :47): inserts tblDealerOnboard (crmSystem='Workiz'), tblWorkizAccounts (dealerId, accountId), updates tblDealer.crmSystem, inserts tblTransmissionDetails (destinationSystem='Workiz', tenantId=dealerId), then invokes jbwagreementsdatamatch for the first sync. The ST-only tblTenant/tblTenantCustomTags/tblTenantCustomFields inserts are skipped (:75-80).

Config stored for a Workiz dealer: tblDealerOnboard, tblWorkizAccounts, tblDealer.crmSystem, tblTransmissionDetailsno clientId/secret/tenantId/custom fields/tags.

FlowTriggerPath
First sync (onboarding)jbwnewonboardingjbwagreementsdatamatchLocations → tblWorkizLocationData; JBW agreements → tblJBWAgreementData; CALL jbw_st_dataSync(dealerId,'Workiz')
Manual re-syncUI → GET /agreementsync/{id}/spcallSame matcher, Workiz branch when accountId present
Job/invoice pullprod-workizdatapull (daily) → prod_jbwworkiztenantifno (untemplated) → SQS prodWorkizTenantsjbwworkizjobsdatafetchjbwworkizjobsdatasaveDone-jobs incremental pull (utc_updated) → tblInvoice/tblInvoiceItem → agreement-equipment update → transmission bookkeeping
Job push (write-back)prod-workizdatapush (daily) → prod_jbwworkizjobdatapush (untemplated), fed by vwWorkizDataPushPushes agreement/claim data into Workiz — source at jbw-sam/src/jbwworkizjobdatapush/ since 2026-07-16
Location pulljbwworkizjoblocationsdatapull (untemplated) → tblWorkizLocationDataCannot persist as written — see caveats

UI Differences (Workiz vs ServiceTitan dealer)

Section titled “UI Differences (Workiz vs ServiceTitan dealer)”
WhereDifference
DealerDetails.jsx:178Tenants panel hidden (no tenant concept)
AgreementsGrid.jsx:570-577, 630Invoice links go to WZUrl + invoiceId; “New Agreement” button ST-only
ClaimsGrid.jsx:661-668Product-code links go to WZUrl
EquipmentsTable.jsx:334Equipment selection checkboxes ST-only
UserRegistrationDialog.jsx:516-520tenants/businessUnits sent empty for Workiz
DealerInfo.jsx:32-35Workiz logo rendered from crmSystem
DealerForm.jsx:85New dealers are hardcoded crmSystem:'ServiceTitan' — Workiz is only assignable via the onboarding wizard

The credential risks are consolidated in the Security & drift callout at the top of this page; the table below is the location index.

WhatWhere
Workiz API token (jb_warranties_7a9f…)Hardcoded in 3 source files (see Connection Model) — not in any .envHardcoded credential
JBW_API_TOKENPlaintext in template.yaml:1115 (env of jbwagreementsdatamatch)Plaintext secret in IaC
JBW_WORKIZ_JOBS_DATA_SAVEtemplate.yaml:1648 — target of the fetch→save invoke
GET_WORKIZ_TENANT_INFOReferenced in jbwdealeradmintoolsupdate/index.js:35Never defined anywhere — dangling
REACT_APP_WZ_URLjbwui .env/stage.env/prod.env
  • Equipment is inferred, not pulled: there is no Workiz equipment API in use — equipment comes solely from job line items with Type === "equipment"; a missingEquipments flag is computed per invoice.
ServiceTitanWorkiz
AuthOAuth2 client-credentials per tenantOne shared static token, Account-Id header
DirectionPull + rich write-back (tags/fields/notes/warranty dates/claims)Pull + write-back via jbwworkizjobdatapush (untemplated)
Equipmentinstalled-equipment APIInferred from job line items
Custom fields/tagsRequired, verified at onboardingNone
Tenant modelDistinct ST tenantIddealerId doubles as tenantId
Onboarding5 steps (creds + custom fields)4 steps (Account ID only)
Scheduled pull producerprod_getTenantInfo → SQSprod_jbwworkiztenantifno → SQS — deployed but not in repo