TL;DR & build order
Every home service CRM in the market falls into one of four buckets. Build native against the top three, bridge the long tail with Zapier, and start the ServiceTitan partner application today because the approval queue is the only true bottleneck.
| Priority | CRM | Why now | Auth | Effort | Path |
|---|---|---|---|---|---|
| 1 | Housecall Pro | 40k+ SMB contractors, purpose-built Lead API, lands in Job Inbox | OAuth 2.0 | 2-3 days | Native + marketplace listing |
| 2 | Jobber | 250k+ users, self-serve GraphQL API, auto-tags lead source | OAuth 2.0 | 3-4 days | Native + app marketplace |
| 3 | Workiz | 120k+ phone-centric SMBs, simplest auth on the list | Token + secret | 2 days | Native |
| 4 | Zapier trigger | Bridges 5000+ apps in one build. ServiceTitan, FieldEdge, Salesforce all reachable through it | N/A | 3-5 days | Publish on Zapier Platform |
| 5 | ServiceTitan | 8k enterprise customers, highest ACV, Bookings API is exactly right | OAuth 2.0, gated | 4-5 days + 4-12 wk approval | Apply NOW, build later |
| 6 | GoHighLevel | Massive SMB adoption, unified contact API, Victor already knows the platform | OAuth 2.0 (V2) | 2 days | Native + marketplace |
| 7 | HubSpot | Free tier covers huge addressable SMB market, best API on the list | OAuth 2.0 | 1-2 days | Native + marketplace |
Universal lead payload
Every CRM expects different field names. The clean play is to define ONE canonical CallSetter lead payload, then write thin per-CRM adapters that map it. This is the contract every integration code path consumes.
{
"schema_version": "1.0",
"event": "lead.qualified",
"event_id": "uuid-v4-idempotency-key",
"timestamp_utc": "2026-05-26T14:32:00Z",
"callsetter_account_id": "acct_xyz",
"call": {
"call_id": "call_abc123",
"direction": "inbound",
"duration_seconds": 142,
"started_at": "2026-05-26T14:29:38Z",
"ended_at": "2026-05-26T14:32:00Z",
"recording_url": "https://storage.callsetter.ai/recordings/call_abc123.mp3",
"transcript_url": "https://storage.callsetter.ai/transcripts/call_abc123.txt",
"transcript_summary": "No heat. Furnace 12 years old. Wants same-week appointment."
},
"contact": {
"first_name": "Maria",
"last_name": "Gonzalez",
"phone": "+13055550182",
"email": "maria@example.com",
"address": {
"street": "1420 NW 7th Ave", "city": "Miami",
"state": "FL", "zip": "33136", "country": "US"
}
},
"service_request": {
"service_type": "HVAC",
"sub_type": "heating_repair",
"urgency": "same_day",
"issue_description": "No heat. Furnace not turning on.",
"preferred_schedule": "2026-05-27T08:00:00",
"is_existing_customer": false
},
"ai_analysis": {
"sentiment": "neutral",
"conversion_intent_score": 0.87,
"qualification_status": "qualified",
"disqualification_reason": null,
"qualifier_answers": {
"owns_home": true,
"issue_type": "heating_repair",
"has_existing_service_contract": false,
"urgency_window": "same_week"
}
},
"routing": {
"destination_crm": "housecall_pro",
"crm_account_id": "hcp_tenant_4829",
"assigned_to": null
}
}
Key design decisions
event_id(UUID v4) is the idempotency key. Safe to retry without creating duplicate leads in the CRM.recording_urlandtranscript_urlare pre-signed time-limited URLs (24h expiry).conversion_intent_score(0.0 to 1.0) lets CSRs prioritize leads in the inbox.urgencyenum:emergency,same_day,same_week,flexible.- Only
qualifiedcalls fire this webhook. Unqualified calls fire a separate low-priority event.
Per-CRM adapter pattern
Each CRM gets one adapter file. Adapter signature:
async function pushToHCP(payload) {...}
async function pushToST(payload) {...}
async function pushToJobber(payload) {...}
Common code (validation, retry, DLQ, signature, logging) stays in the orchestrator. The adapter only knows its CRM's field shape.
Customer onboarding UX
| Tier | Method | Used for | Friction |
|---|---|---|---|
| Best | OAuth click-through | HCP, Jobber, ServiceTitan, GHL, HubSpot, Pipedrive | One button, redirect, done |
| Common | API key paste with wizard + live validation | Workiz, FieldEdge, Notion, Airtable | Branded wizard with "open your CRM, copy key" steps |
| Fallback | Webhook URL paste | Anything via Zapier / Make / n8n bridge | Customer pastes the URL into their automation tool |
Recommended onboarding flow
Sign Up
-> Choose CRM (dropdown with logos)
-> [HCP/Jobber/GHL/HubSpot] OAuth click-through
-> [Workiz/FieldEdge] API key paste wizard with live validation
-> [Zapier] Embed Zapier widget with pre-built Zap template
-> [Other] Copy webhook URL, paste into CRM or automation tool
-> Fire a test lead
-> Confirm lead appeared in CRM
-> Done
The Zapier embed widget (Zapier Partner API) lets customers connect a Zap without leaving the CallSetter dashboard. Significantly reduces drop-off.
Housecall Pro
Difficulty 2/5 ~40,000 SMB contractors MAX plan only for API
API access & plan gating
Public REST API at docs.housecallpro.com. Separate Partner Jobs API at docs.housecallpro.com/docs/partner-jobs/ for lead-source integrations specifically.
Plan gating is critical. API access locked to the MAX plan only ($329/mo, 8 users). Basic ($79/mo) and Essentials ($189/mo) have zero API access. Webhooks also MAX-only. Only Admin users can generate keys. No dedicated developer support from HCP engineers.
Authentication: two methods
Method A: API key (server-to-server, single tenant). Header format: Authorization: Token 0046421dc0ba47ec87bef0c089415380. Customer generates: HCP -> App Store -> API card -> Generate Key -> Name -> Copy. Grants access to ALL data in the account.
Method B: OAuth 2.0 (multi-tenant marketplace, REQUIRED for CallSetter scale). Authorization at pro.housecallpro.com/oauth/authorize, token exchange at api.housecallpro.com/oauth/token.
apideveloper@housecallpro.com with your CALLBACK_URL. Manual approval, 1 to 5 business days. Not self-serve. Start this email today.Endpoints
Base: https://api.housecallpro.com/v1/
| Endpoint | Use | Notes |
|---|---|---|
POST /customers | Create customer | No auto-dedup. Search by phone first. |
POST /jobs | Create job | Requires existing customer_id + address_id. |
POST /leads USE THIS | Create lead | Lands in Job Inbox -> API Leads channel. Dispatcher reviews + converts. Matches existing workflow. |
POST /jobs/{id}/notes | Attach transcript | Recording URL embedded as link. |
POST /job_links | No-auth share URL | Drops a lead into an HCP account without needing API key. Investigate for non-MAX customers. |
Reference implementation
const HCP_BASE = 'https://api.housecallpro.com/v1';
const headers = { 'Authorization': `Token ${token}`, 'Content-Type': 'application/json' };
async function pushLeadToHCP(payload) {
// 1. Dedup check by phone
const r = await fetch(`${HCP_BASE}/customers?mobile_number=${encodeURIComponent(payload.contact.phone)}`, { headers });
const { customers } = await r.json();
let customerId;
if (customers?.length > 0) {
customerId = customers[0].id;
} else {
// 2. Create customer
const c = await fetch(`${HCP_BASE}/customers`, {
method: 'POST', headers,
body: JSON.stringify({
first_name: payload.contact.first_name,
last_name: payload.contact.last_name,
mobile_number: payload.contact.phone,
email: payload.contact.email,
address: { ...payload.contact.address, country: 'US' },
tags: ['callsetter-ai'],
notifications_enabled: true,
}),
}).then(r => r.json());
customerId = c.id;
}
// 3. Create LEAD (Job Inbox -> API Leads channel)
return fetch(`${HCP_BASE}/leads`, {
method: 'POST', headers,
body: JSON.stringify({
customer_id: customerId,
name: `${payload.contact.first_name} ${payload.contact.last_name}`,
phone_number: payload.contact.phone,
description: `[CallSetter AI] ${payload.service_request.service_type} | Urgency: ${payload.service_request.urgency}\nRecording: ${payload.call.recording_url}`,
lead_source: 'CallSetter AI',
}),
}).then(r => r.json());
}
Webhooks (MAX only)
Events: customer.*, job.created/scheduled/started/on_my_way/completed/canceled/paid, estimate.*, lead.created/updated/converted/lost/deleted, pro_created.
For CallSetter, the high-value subscriptions are lead.converted (closes the attribution loop), job.scheduled (trigger confirmation SMS), job.completed + job.paid (revenue attribution).
Webhook payload note: includes event type + resource ID, NOT the full record. Follow up with GET /jobs/{id} for state. HMAC-SHA256 signed.
Marketplace listing
HCP integrations page lists ~30 partners (Thumbtack, CompanyCam, CallRail, Angi). Path: same apideveloper@housecallpro.com email. Relationship-driven, no self-serve form. CallSetter's profile (AI voice agent for HVAC/plumbing/roofing) is a clean fit next to CallRail and Angi.
ServiceTitan
Difficulty 3/5 ~8,000 enterprise contractors Partner approval gated
Two API access paths
Custom Integration (single-tenant): contractor admin -> Settings -> Integrations -> API Application Access -> Connect New App -> approve scopes -> generates Client ID + Client Secret. <20 min. No marketplace listing required. Use this for first 10-20 contractors.
Marketplace (multi-tenant): required for CallSetter at scale. Vendor registers once at developer.servicetitan.io, obtains App Key. Three certification tiers (Silver / Gold / Titanium), 5-stage process, annual dues, annual re-cert. 4 to 12 week approval timeline. Contact: integrations@servicetitan.com.
Authentication
OAuth 2.0 client credentials grant (machine-to-machine, no user login). No refresh tokens. Tokens expire in 900 sec (15 min). Cache per-tenant or get throttled.
| Env | Auth URL | API Base |
|---|---|---|
| Integration (sandbox) | auth-integration.servicetitan.io/connect/token | api-integration.servicetitan.io |
| Production | auth.servicetitan.io/connect/token | api.servicetitan.io |
Required on every API call: Authorization: Bearer {token} AND ST-App-Key: {app_key} (distinct from Client credentials).
Endpoints (use Bookings)
Pattern: {base}/{namespace}/v2/tenant/{tenant_id}/{resource}
| Endpoint | Use |
|---|---|
POST /crm/v2/tenant/{tid}/bookings PRIMARY | Lands on Calls screen for CSR. Min: name + (address/phone/email). Pre-fill jobTypeId, businessUnitId, campaignId, source, summary. |
POST /crm/v2/tenant/{tid}/customers | Customer record. Required: name, type, contact. |
POST /crm/v2/tenant/{tid}/locations | Job site address. |
POST /jpm/v2/tenant/{tid}/jobs | Job. Required: customerId, locationId, jobTypeId, businessUnitId, priority. |
POST /jpm/v2/tenant/{tid}/appointments | Scheduled time slot. |
Reference implementation
# Get access token
TOKEN=$(curl -s -X POST "https://auth.servicetitan.io/connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=${ST_CLIENT_ID}&client_secret=${ST_CLIENT_SECRET}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
# Create booking from CallSetter
curl -s -X POST "https://api.servicetitan.io/crm/v2/tenant/${TENANT_ID}/bookings" \
-H "Authorization: Bearer ${TOKEN}" \
-H "ST-App-Key: ${ST_APP_KEY}" \
-H "Content-Type: application/json" \
-d '{
"source": "CallSetter AI",
"name": "John Martinez",
"address": { "street": "4521 Oak Ridge Dr", "city": "Phoenix", "state": "AZ", "zip": "85016", "country": "USA" },
"contacts": [
{ "type": "Phone", "value": "+16025551234", "memo": "Mobile" },
{ "type": "Email", "value": "john.martinez@gmail.com" }
],
"jobTypeId": 12345,
"businessUnitId": 67890,
"campaignId": 11111,
"summary": "AC not cooling. 3-ton Carrier 2019. Recording: https://storage.callsetter.ai/rec/abc123.mp3",
"isFirstTimeClient": true,
"bookingProviderId": 99
}'
Webhooks V2
V1 sunsetting. V2 at developer-next.servicetitan.io/docs/webhooks. Subscribe via POST /webhooks/v2/register. HMAC-SHA256 verification via x-servicetitan-signature header. Exponential backoff, up to 3 retries. Ack with 200 immediately, process async.
Rate limits
60 calls/sec per app per tenant. Reporting APIs 5/min same report. CallSetter's 3-5 calls per completed call is well below. Token requests count toward limit, so cache per-tenant.
The #1 gotcha: tenant-specific enum IDs
businessUnitId, jobTypeId, campaignId, bookingProviderId are integer IDs unique PER contractor. "Residential HVAC" might be 12345 on one tenant, 98765 on another. Onboarding must query and store the mapping:
GET /settings/v2/tenant/{tid}/business-units
GET /settings/v2/tenant/{tid}/job-types
GET /settings/v2/tenant/{tid}/campaigns
GET /crm/v2/tenant/{tid}/booking-provider-tags
Present these as dropdowns during CallSetter onboarding. Mismatched jobType / businessUnit pair causes CSR dropdown to silently fail to pre-fill.
Jobber
Difficulty 2/5 ~250,000 pros Connect plan+ for API
API type
GraphQL API at https://api.getjobber.com/api/graphql (single endpoint, no REST). Version pinned per-request via X-JOBBER-GRAPHQL-VERSION: 2023-11-15 header. Available on Connect plan and above (not Core). 90-day free dev trial.
Authentication: OAuth 2.0
Authorization Code Grant. No API keys. Access tokens expire in 60 min, refresh tokens long-lived. Scopes for CallSetter: clients:read/write, jobs:read/write, requests:read/write, quotes:read/write.
Mutations
mutation CreateClient($input: ClientCreateInput!) {
clientCreate(input: $input) {
client { id firstName lastName phones { number primary } }
userErrors { message path }
}
}
# Variables
{
"input": {
"firstName": "Maria", "lastName": "Gonzalez",
"phones": [{ "number": "+13055550182", "description": "MAIN", "primary": true }],
"emails": [{ "address": "maria@example.com", "description": "MAIN", "primary": true }],
"address": {
"street1": "1420 NW 7th Ave", "city": "Miami",
"province": "FL", "postalCode": "33136"
},
"notes": "Inbound call via CallSetter AI. Service: HVAC repair. Recording: https://..."
}
}
clientCreate. Cannot be edited by Jobber users. CallSetter AI gets credit for every lead it pushes, built in.For the service request itself, follow with requestCreate (matches Jobber's Requests pipeline stage, the intake step before Quote/Job) OR jobCreate (skip the request stage, go straight to a scheduled job).
Webhooks
App-level (not per-account). Must ack within 1 second. At-least-once delivery (use itemId for idempotency). HMAC-SHA256 signed via X-Jobber-Hmac-SHA256 header.
Topics: CLIENT_CREATE/UPDATE/DESTROY, REQUEST_CREATE, QUOTE_CREATE, QUOTE_APPROVAL, JOB_COMPLETE, INVOICE_CREATE, APP_CONNECT.
Rate limits
Per app / per Jobber account: DDoS middleware 2500 req/5 min (HTTP 429). GraphQL cost bucket: 10000 points max, 500/sec restored. CallSetter's per-call cost well under 100 points.
App Marketplace
Three-stage: (1) Draft state allows up to 5 paying Jobber accounts without review (covers initial pilots), (2) App review submitted via Developer Center, (3) ~2-week beta with selected customers, then published. 2FA required on developer account.
Workiz
Difficulty 2/5 ~120,000 customers All plans
- API: REST at
https://api.workiz.com/api/v1/{token}/ - Auth: Token + secret pair. Token embedded in URL path; secret in header/query. No OAuth.
- Lead create:
POST /{token}/job/create/creates a job (= lead in Workiz). Separate/client/create/for client record. - Webhooks: Yes. Configure via Settings -> API & Webhooks.
- Docs: developer.workiz.com (partial).
- Zapier + Make.com: native modules exist.
FieldEdge
Difficulty 4/5 ~5,000 customers Partner-gated API
Owned by Xplor Technologies (acquired 2021). API exists at docs.api.fieldedge.com (Azure API Management) but is gated to "integrated partners" only. No self-serve.
Pragmatic path: ship via Zapier today (native FieldEdge connector with "Create Work Order" action), submit partner application in parallel at fieldedge.com/partners for official API access later.
Known endpoint patterns (partner portal): POST /sessions, GET /customers, POST /work-orders, PATCH /work-orders/{id}/status, GET /appointments. No public webhooks. Customer creation typically a side-effect of work-order creation.
GoHighLevel (V2)
Difficulty 2/5 Massive SMB home services V1 sunset 2025-12-31
- API: REST at
https://services.leadconnectorhq.com. Sub-account (location) scoped,locationIdrequired. - Auth: OAuth 2.0 (marketplace app, recommended for CallSetter SaaS) or Private Integration tokens (agency/location).
- Lead create:
POST /contacts/with{ firstName, lastName, phone, email, locationId, source: "CallSetter AI", tags: ["inbound-call"] }. Unified contact model, no separate "lead" object. - Webhooks: 50+ events including
ContactCreate,InboundWebhookTriggered,AppointmentCreate. - Docs: highlevel.stoplight.io/docs/integrations
HubSpot
Difficulty 1/5 (easiest on the list) Free tier API access
- API: REST at
https://api.hubapi.com/. Available on Free + all paid plans. - Auth: Private App Token (Bearer, single account) or OAuth 2.0 (multi-tenant). Legacy API keys retired 2023.
- Lead create:
POST /crm/v3/objects/contactsatomic.{ properties: { firstname, lastname, phone, email, hs_lead_status, ... } }. No secondary step. - Webhooks: Yes, free tier. Contact property changes + creation.
- Rate: 100 req/10s free, higher on paid.
- Docs: developers.hubspot.com
Best API on this entire list. Atomic contact creation, robust webhooks on free tier, well-documented. Highest leverage build per dev-day spent.
Pipedrive
Difficulty 2/5 All plans including Essential
- API: REST at
https://api.pipedrive.com/v1/ - Auth: API token (legacy) or OAuth 2.0 (recommended for SaaS).
- Lead create: Two-step.
POST /personsthenPOST /leadsreferencingperson_id. - Webhooks: Yes via
POST /webhooks. Real-time events for leads, deals, persons. - Rate: 80 req/10s per token.
- Docs: developers.pipedrive.com
Other CRMs (matrix)
| CRM | API | Auth | Lead create | Webhooks | Zapier | Difficulty |
|---|---|---|---|---|---|---|
| Service Fusion | REST, Pro plan+ | OAuth 2.0 Client Credentials | POST /customers then POST /jobs | No | Yes | 3/5 |
| mHelpDesk | REST | HTTP Basic | POST /api/v1/customers, /jobs | Yes | Yes | 3/5 |
| BookingKoala (cleaning) | REST, partial | API key Bearer | Unconfirmed inbound endpoint | Outbound only | Unconfirmed actions | 4/5 |
| Zoho CRM | REST, Standard+ | OAuth 2.0 (DC-aware) | POST /crm/v8/Leads via Zoho-oauthtoken header | Yes | Yes | 3/5 |
| Salesforce | REST | OAuth 2.0 Connected App | POST /sobjects/Lead/ (Company REQUIRED) | SOAP / Platform Events / CDC | Yes | 4/5 |
| Monday.com | GraphQL | OAuth 2.0 Bearer | create_item mutation (board_id + column schema per customer) | Yes | Yes | 3/5 |
| Pipefy | GraphQL | OAuth 2.0 / Service Account | createCard mutation (pipe_id + field_ids per customer) | Yes | Yes | 3/5 |
| Airtable | REST | PAT Bearer or OAuth 2.0 | POST /v0/{baseId}/{tableId} | Yes (since 2022) | Yes | 2/5 |
| Notion | REST | Integration Token or OAuth 2.0 | POST /v1/pages with database_id parent | No (poll) | Yes (poll) | 3/5 |
| Service Autopilot | Closed | N/A | No public path | No | No | 5/5 (manual) |
| ResponsiBid | Outbound only | N/A | No inbound path (ResponsiBid is the source, not the destination) | Outbound only | Outbound only | N/A |
iPaaS: Zapier, Make.com, n8n
Why publish a Zapier app at all
Even with native HCP / Jobber / Workiz adapters, the long tail of CRMs (Salesforce, Zoho, Monday, Notion, Pipedrive, Service Fusion, FieldEdge, mHelpDesk, etc.) is best reached via Zapier. One Zapier app = 5,000+ destinations.
What to build on Zapier Platform
- Trigger:
New Qualified Leadas a REST Hook (instant fire, no polling). Payload is the universal lead schema above. - Optional action:
Update Lead Statusfor future two-way sync.
Publishing path
- Register at zapier.com/developer-platform/integrations
- Choose Platform UI (no-code, browser) for speed OR Platform CLI (
zapier-platform-clinpm) for version control - Define trigger as REST Hook
- Submit -> Zapier reviews ~1 week -> 90-day Beta phase -> Public. Early exit: one signup via embedded widget unlocks Public immediately.
Zapier vs Make.com
| Factor | Zapier | Make.com |
|---|---|---|
| App directory | ~5,000 | ~2,000 |
| Customer cost | $30-70/mo | ~$10/mo |
| Latency | REST Hook instant | Webhook instant |
| Learning curve | Low | Medium |
| Best for | Enterprise / mid-market | SMB |
Build Zapier first. Add Make.com as the second option once Zapier ships. Surface both in onboarding.
n8n self-host (V2 or later)
Appropriate at 100+ customers when Zapier/Make per-customer fees or brittleness become measurable. CallSetter runs n8n internally, each customer = a workflow, CallSetter posts to https://n8n.callsetter.ai/webhook/{customer-id}. Full delivery visibility. Not the MVP choice.
Retry & dead-letter queue
Never drop a lead because the customer's CRM was down at 2 AM.
Retry schedule (exponential backoff with jitter)
| Attempt | Wait |
|---|---|
| 1st retry | 30s ± 10s |
| 2nd retry | 2m ± 20s |
| 3rd retry | 10m ± 1m |
| 4th retry | 1h ± 10m |
| 5th retry | 6h ± 30m |
| Give up | -> DLQ |
HTTP 4xx errors -> straight to DLQ (auth/endpoint bad, not retriable). Only 5xx and timeouts retry.
DLQ preserves
Full original payload, all request headers, HTTP status code + error body per attempt, retry count, timestamps per attempt, event_id (idempotent replay).
DLQ management
- Show DLQ events in CallSetter admin dashboard per customer.
- Alert when 3+ DLQ events accumulate (auth expired or CRM account suspended).
- Replay after customer re-authenticates, batched max 10/min to respect CRM rate limits.
Two-way sync (V2)
V1 = one-way push (CallSetter -> CRM). Covers 95% of customer value. Simple, debuggable, no inbound API surface to secure. Build first.
Two-way matters for two scenarios:
- Job Won callback. CRM marks job invoiced -> push revenue back to CallSetter -> closes attribution loop -> powers "calls-to-revenue" reporting in CallSetter dashboard. High product value. Medium engineering cost.
- Booking confirmed. CRM books slot -> update CallSetter call record -> follow-up calls have context.
Cost: roughly 3-5x V1 effort (per-CRM inbound webhook, signature verification, internal record updates).
Verdict: ship V1 one-way first. Build Job Won callback in V2 when customers start asking "did my calls turn into jobs?"
Immediate actions
This week
- Apply for ServiceTitan developer access at developer.servicetitan.io/request-access. 4-12 wk approval. Start the slot today.
- Email
apideveloper@housecallpro.comrequesting OAuth credentials + technology partner listing conversation. Send your callback URL. 1-5 business days. - Email
integrations@servicetitan.comto open Silver-tier marketplace conversation in parallel with the dev access request. - Lock the universal lead payload schema (above). Every adapter consumes this contract.
Build order (weeks 1-4)
- Lock universal lead payload schema + Postgres outbox + retry orchestrator + DLQ (or wire Hookdeck for V1 speed).
- HCP native adapter via Lead endpoint + OAuth. Dogfood on 1-2 friendly contractor pilots.
- Jobber native adapter via clientCreate + requestCreate. App registered on Jobber Developer Center, Draft state pilots.
- Workiz native adapter (simplest auth, fastest to ship).
- Zapier app published with REST Hook trigger.
- HubSpot + GoHighLevel native adapters (free tiers = huge addressable market).
Build order (weeks 5-12)
- Pipedrive native adapter.
- ServiceTitan native (after partner approval lands).
- Salesforce + Zoho via Zapier (avoid the OAuth + per-org instance complexity for V1).
- Customer onboarding UX polish: OAuth click-throughs, API key wizard, Zapier embed widget.
- Two-way V2: Job Won callback from HCP, Jobber, ServiceTitan.
- Marketplace listings: Jobber published state, HCP integrations page, ServiceTitan Silver certification.