What Hatch's ServiceTitan integration is, how it works, and where to begin.
What does the ServiceTitan integration do?
Hatch offers a native integration with ServiceTitan that you can activate within the App Marketplace (located in your Hatch workspace). This integration syncs contact data from ServiceTitan to Hatch, and can also sync Hatch events to ServiceTitan.
While active, the ServiceTitan integration will sync new and updated ServiceTitan data every 15 minutes. It will also sync Hatch events back to ServiceTitan as soon as they occur (if you have enabled this feature).
To get started, open the App Marketplace in your Hatch workspace and follow the setup instructions below.
How do you set up the integration?
Requirements
You will need the following to set up the integration:
- Hatch account with manager privileges
- ServiceTitan account with admin privileges on your tenant
- This account must be able to access the ServiceTitan My Apps developer page (https://developer.servicetitan.io/custom/my-apps/)
Setup Steps
Before you begin, consider reviewing ServiceTitan's documentation around creating apps and managing client IDs and secrets. However, all of the necessary information is included in the instructions below.
Additionally, the Hatch integration requires specific API scopes (listed below). These provide Hatch with the necessary permissions to read and update data in your ServiceTitan tenant. The scopes below are only the bare minimum required by our integration.
The following is the set of steps required to activate the integration:
- In Hatch, open the App Marketplace.
- Under the CRMs tab, click the ServiceTitan Connect button.
- Proceed through the setup until you reach the connection configuration screen.
- Open the login page for ServiceTitan’s My Apps tool at https://developer.servicetitan.io/signin.
- Click Login as Production Environment User and log in using your ServiceTitan credentials.
- Review and accept ServiceTitan’s API terms of use (if prompted).
- Click the New App button in the top right.
- For both Application Name and Organization, enter: Hatch
- For Homepage, enter: https://www.usehatchapp.com
- You should see your tenant ID pre-populated under Tenant(s).
- If you only have one ServiceTitan tenant (or are unfamiliar with this concept), do not modify this section.
- If you have multiple ServiceTitan tenants, add the IDs for any tenants that you want to grant access to Hatch. However, we recommend configuring one tenant per app.
- Under API Scopes, click All next to each section so that every box on the page is checked.
- Optionally — if you do not want to provide all scopes — you can only select the scopes that are required by the integration:
- The following Read scopes are required:
- Accounting: Payment Types, Tax Zones
- CRM: Booking Provider Tags, Bookings, Customers, Leads, Locations, Tags
- Dispatch: Appointment Assignments, Non-Job Appointments, Zones
- Job Planning and Management: Appointments, Job Cancel Reasons, Job Hold Reasons, Jobs, Job Types
- Memberships: Customer Memberships, Membership Types, Recurring Service Events, Recurring Services, Recurring Service Types
- Marketing: Campaigns, Campaign Categories
- Sales & Estimates: Estimates
- Settings: Business Units, Employees, Tag Types, Technicians, User Roles
- The following Write scopes are required:
- CRM: Customers, Leads, Locations
- Job Planning and Management: Jobs
- The following Read scopes are required:
- Optionally — if you do not want to provide all scopes — you can only select the scopes that are required by the integration:
- Click Create App at the bottom.
- Click Edit next to the new app that is now listed.
- Copy/paste the Application Key into the associated field on the Hatch setup screen.
- You may now close the My Apps tool.
- Open the ServiceTitan instance that you want to connect to your Hatch workspace.
- Navigate to Settings → Integrations → API Application Access.
- Click the Connect New App button, accept the terms and conditions (if prompted), and select Hatch from the list of apps.
- With Hatch selected, click Connect.
- Scroll to the bottom of the screen that appears.
- For any select boxes listed, choose No restriction if it is available (if not, select the first available option).
- Click Allow Access.
- On the resulting Application Details screen, copy/paste the Tenant ID and the Client ID into the associated fields on the Hatch setup screen.
- Click the Generate button next to Client Secret and then click Yes, Continue to generate the secret.
- Copy/paste the Client Secret into the associated field on the Hatch setup screen.
- You may now close any open ServiceTitan pages. All of the remaining steps are completed in Hatch.
- In the Hatch setup screen, select which opportunity models you would like to enable.
- We recommend only enabling one opportunity model per integration.
- Complete any remaining setup steps, connecting Hatch and activating your integration.
- Once complete, the integration will begin syncing your last 90 days of data.
Setup Options
These options can be set during the integration setup:
- Choose which ServiceTitan data syncs to Hatch — which opportunity models the integration should sync from ServiceTitan
- Choose where outgoing campaign messages should sync — if/where Hatch should create ServiceTitan notes for outgoing campaign message events
- Sync message content for outgoing campaign messages — if the message content should be included in notes created for outgoing campaign message events
- Choose where incoming messages & calls should sync — if/where Hatch should create ServiceTitan notes for incoming messages & calls
- Sync message content for incoming messages & calls — if the message content should be included in notes created for incoming messages & calls
- Choose where outgoing manual messages & calls should sync — if/where Hatch should create ServiceTitan notes for outgoing manual messages & calls
- Sync message content for outgoing manual messages & calls — if the message content should be included in notes created for outgoing manual messages & calls
Best Practices
- We recommend using one ServiceTitan app per tenant, creating additional app instances in My Apps for any additional tenants (following the steps here). This is due to ServiceTitan API rate limits. As of August 2023, configured apps may not exceed 60 requests per second. Therefore, if too many tenants are using the same app (and, by extension, sending too much data), the API will begin blocking requests and the Hatch integration will not work as expected. You can read more about ServiceTitan API limitations here.
- We recommend enabling only one opportunity model in your integration configuration. If you enable multiple, be aware that you may see multiple ServiceTitan opportunities on the same contact (e.g. a job and a membership on the same person's Hatch contact). If you need multiple types of ServiceTitan data, we recommend configuring separate integration instances across multiple Hatch departments. Your CS representative can assist with this configuration.
When does Hatch sync data to/from ServiceTitan?
ServiceTitan → Hatch
ServiceTitan data can be sync’d to Hatch in the following scenarios. These options are configured during the integration setup.
- If a Job record is created or updated in ServiceTitan, Hatch syncs the Job record along with any associated data (see below for specific fields)
- If a Membership record is created or updated in ServiceTitan, Hatch syncs the Membership record along with any associated data (see below for specific fields)
Hatch → ServiceTitan
Hatch events can be sync’d to ServiceTitan in the following scenarios. These options are configured during the integration setup.
- When a Hatch campaign sends a text/email/voicemail to a contact, Hatch creates a Note for the event on the associated Job, Customer, or Location
- When a contact calls or sends a text/email/voicemail to a Hatch workspace, Hatch creates a Note for the event on the associated Job, Customer, or Location
- When a Hatch user calls or sends a text/email to a contact, Hatch creates a Note for the event on the associated Job, Customer, or Location
- When events occur within a Hatch campaign, Hatch creates a Note for the event on the associated Job, Customer, or Location
- A contact is launched (added) to a Hatch campaign
- A contact is sent the first message of a Hatch campaign
- A contact is removed from a Hatch campaign before it has ended
- A contact completed a Hatch campaign
How is ServiceTitan data stored in Hatch?
Opportunity Types
There are two types of Hatch opportunities that the ServiceTitan integration creates:
- Job Opportunity — Created from ServiceTitan Job records, this is the primary opportunity type in our ServiceTitan integration. Additional related data is also included (see below).
- Membership Opportunity — These opportunities are created from ServiceTitan Membership records. Additional related data is also included (see below).
Opportunity Creation & Update
When an opportunity is generated that doesn’t match an existing contact, a new contact will be created. A new opportunity will be added to an existing contact if a match is found.
The integration will replace an existing Hatch opportunity when:
- The sync’d Hatch External ID matches the existing opportunity’s Hatch External ID (see below for how the External ID is mapped)
The integration will create an additional (new) Hatch opportunity when:
- The sync’d Hatch External ID does not match the existing opportunity’s Hatch External ID (see below for how the External ID is mapped)
What ServiceTitan data is available in Hatch?
Detail Field Lists
Some fields below are lists. Data within a list field is sorted by “created date” in descending order. For example, a list of estimates would have the most recently-created estimate listed first.
When accessing a list, you need to provide the index number of the list item that you want. Because this index number will vary, it is shown in the lists below with the placeholder #.
The following fields are available in the opportunity details:
Job Opportunity
Field | Type | Required? |
---|---|---|
appointments | List | Yes |
appointments:#:appointmentAssignments | List | Yes |
appointments:#:appointmentAssignments:#:appointmentId | String (Number) | Yes |
appointments:#:appointmentAssignments:#:assignedById | String (Number) | Yes |
appointments:#:appointmentAssignments:#:assignedOn | Index 1: Date Index > 1: String (Date) |
Yes |
appointments:#:appointmentAssignments:#:id | String (Number) | Yes |
appointments:#:appointmentAssignments:#:isPaused | Index 1: Boolean Index > 1: String (Boolean) |
Yes |
appointments:#:appointmentAssignments:#:jobId | String (Number) | Yes |
appointments:#:appointmentAssignments:#:status | String | Yes |
appointments:#:appointmentAssignments:#:technicianId | String (Number) | Yes |
appointments:#:appointmentAssignments:#:technicianName | String | Yes |
appointments:#:appointmentNumber | String | No |
appointments:#:arrivalWindowEnd | Index 1: Date Index > 1: String (Date) |
No |
appointments:#:arrivalWindowStart | Index 1: Date Index > 1: String (Date) |
No |
appointments:#:createdOn | Index 1: Date Index > 1: String (Date) |
Yes |
appointments:#:end | Index 1: Date Index > 1: String (Date) |
Yes |
appointments:#:id | String (Number) | Yes |
appointments:#:modifiedOn | Index 1: Date Index > 1: String (Date) |
Yes |
appointments:#:specialInstructions | String | No |
appointments:#:start | Index 1: Date Index > 1: String (Date) |
Yes |
appointments:#:status | String | Yes |
businessUnit | Object | Yes |
businessUnit:active | Boolean | Yes |
businessUnit:id | String (Number) | Yes |
businessUnit:name | String | Yes |
businessUnit:officialName | String | No |
campaign | Object | Yes |
campaign:active | Boolean | Yes |
campaign:id | String (Number) | Yes |
campaign:name | String | Yes |
cancelReasons | List | Yes |
cancelReasons:#:name | String | Yes |
cancelReasons:#:reasonId | String (Number) | Yes |
cancelReasons:#:text | String | Yes |
completedOn | Date | No |
createdOn | Date | Yes |
customer | Object | Yes |
customer:active | Boolean | Yes |
customer:contacts | List | Yes |
customer:contacts:#:id | String (Number) | Yes |
customer:contacts:#:phoneSettings | Object | Yes |
customer:contacts:#:phoneSettings:doNotText | Index 1: Boolean Index > 1: String (Boolean) |
Yes |
customer:contacts:#:type | String | Yes |
customer:contacts:#:value | String | Yes |
customer:doNotService | Boolean | Yes |
customer:id | String (Number) | Yes |
customer:name | String | Yes |
customer:type | String | Yes |
customFields | List | Yes |
customFields:#:name | String | Yes |
customFields:#:value | String | Yes |
estimates | List | Yes |
estimates:#:active | Index 1: Boolean Index > 1: String (Boolean) |
Yes |
estimates:#:createdOn | Index 1: Date Index > 1: String (Date) |
Yes |
estimates:#:id | String (Number) | Yes |
estimates:#:modifiedOn | Index 1: Date Index > 1: String (Date) |
Yes |
estimates:#:name | String | No |
estimates:#:soldBy | Object | No |
estimates:#:soldBy:active | Index 1: Boolean Index > 1: String (Boolean) |
No |
estimates:#:soldBy:email | String | No |
estimates:#:soldBy:id | String (Number) | No |
estimates:#:soldBy:name | String | No |
estimates:#:soldOn | Index 1: Date Index > 1: String (Date) |
No |
estimates:#:status | Object | No |
estimates:#:status:name | String | No |
estimates:#:status:value | String (Number) | No |
estimates:#:subtotal | Index 1: Number Index > 1: String (Number) |
Yes |
estimates:#:summary | String | No |
jobNumber | String | Yes |
jobStatus | String | Yes |
jobType | Object | Yes |
jobType:id | String (Number) | Yes |
jobType:name | String | Yes |
location | Object | Yes |
location:active | Boolean | Yes |
location:address | Object | Yes |
location:address:city | String | Yes |
location:address:country | String | Yes |
location:address:latitude | Number | No |
location:address:longitude | Number | No |
location:address:state | String | Yes |
location:address:street | String | Yes |
location:address:unit | String | No |
location:address:zip | String | Yes |
location:id | String (Number) | Yes |
location:name | String | Yes |
location:zone | Object | No |
location:zone:id | String (Number) | No |
location:zone:name | String | No |
modifiedOn | Date | Yes |
noCharge | Boolean | Yes |
notificationsEnabled | Boolean | Yes |
priority | String | Yes |
summary | String | No |
tagTypes | List | Yes |
tagTypes:#:active | Index 1: Boolean Index > 1: String (Boolean) |
Yes |
tagTypes:#:id | String (Number) | Yes |
tagTypes:#:name | String | Yes |
Membership Opportunity
Field | Type | Required? |
---|---|---|
active | Boolean | Yes |
billingFrequency | String | Yes |
businessUnit | Object | Yes |
businessUnit:active | Boolean | Yes |
businessUnit:id | String (Number) | Yes |
businessUnit:name | String | Yes |
businessUnit:officialName | String | No |
cancellationDate | Date | No |
createdOn | Date | Yes |
customer | Object | Yes |
customer:active | Boolean | Yes |
customer:contacts | List | Yes |
customer:contacts:#:id | String (Number) | Yes |
customer:contacts:#:phoneSettings | Object | Yes |
customer:contacts:#:phoneSettings:doNotText | Index 1: Boolean Index > 1: String (Boolean) |
Yes |
customer:contacts:#:type | String | Yes |
customer:contacts:#:value | String | Yes |
customer:doNotService | Boolean | Yes |
customer:id | String (Number) | Yes |
customer:name | String | Yes |
customer:type | String | Yes |
duration | Number | No |
followUpOn | Date | Yes |
followUpStatus | String | Yes |
from | Date | No |
id | String (Number) | Yes |
initialDeferredRevenue | Number | Yes |
location | Object | Yes |
location:active | Boolean | Yes |
location:address | Object | Yes |
location:address:city | String | Yes |
location:address:country | String | Yes |
location:address:latitude | Number | No |
location:address:longitude | Number | No |
location:address:state | String | Yes |
location:address:street | String | Yes |
location:address:unit | String | No |
location:address:zip | String | Yes |
location:id | String (Number) | Yes |
location:name | String | Yes |
location:zone | Object | No |
location:zone:id | String (Number) | No |
location:zone:name | String | No |
membershipType | Object | Yes |
membershipType:active | Boolean | Yes |
membershipType:createdOn | Date | Yes |
membershipType:discountMode | String | Yes |
membershipType:id | String (Number) | Yes |
membershipType:locationTarget | String | Yes |
membershipType:modifiedOn | Date | Yes |
membershipType:name | String | Yes |
membershipType:revenueRecognitionMode | String | Yes |
memo | String | No |
modifiedOn | Date | Yes |
nextScheduledBillDate | Date | No |
paymentType | Object | No |
paymentType:id | String (Number) | No |
paymentType:name | String | No |
recurringLocation | Object | Yes |
recurringLocation:active | Boolean | Yes |
recurringLocation:address | Object | Yes |
recurringLocation:address:city | String | Yes |
recurringLocation:address:country | String | Yes |
recurringLocation:address:latitude | Number | No |
recurringLocation:address:longitude | Number | No |
recurringLocation:address:state | String | Yes |
recurringLocation:address:street | String | Yes |
recurringLocation:address:unit | String | No |
recurringLocation:address:zip | String | Yes |
recurringLocation:id | String (Number) | Yes |
recurringLocation:name | String | Yes |
recurringLocation:zone | Object | No |
recurringLocation:zone:id | String (Number) | No |
recurringLocation:zone:name | String | No |
renewalBillingFrequency | String | No |
renewalDuration | String | No |
soldBy | Object | No |
soldBy:active | Boolean | No |
soldBy:email | String | No |
soldBy:id | String (Number) | No |
soldBy:name | String | No |
status | String | Yes |
to | Date | No |
Standard Field Mapping
Hatch standard fields are mapped from ServiceTitan detail fields as detailed below.
Job Opportunity
The base object of this opportunity model is the ServiceTitan Job. Therefore, (as an example) the id field would indicate the ID of the ServiceTitan Job record.
Hatch Standard Field | ServiceTitan Detail Field |
---|---|
The value of the first item in the customer:contacts list that has type equal to email | |
External ID | id |
External Contact ID | customer:id |
External Created At | createdOn |
External Updated At (used for Hatch opportunity sorting) |
The most recent date within these fields: |
First Name | If there is no comma in customer:name: The first word in the customer:name value If there is a comma in customer:name: All text after the first comma in the customer:name value |
Last Name | If there is no comma in the customer:name value: Everything after the first word in the customer:name value If there is a comma in the customer:name value: All text before the first comma in the customer:name value |
Phone | The value of the first item in the customer:contacts list that has type equal to mobilePhone IF ABOVE IS MISSING: The value of the first item in the customer:contacts list that has type equal to phone |
Membership Opportunity
The base object of this opportunity model is the ServiceTitan Membership. Therefore, (as an example) the id field would indicate the ID of the ServiceTitan Membership record.
In Membership opportunities, the following Hatch standard fields are mapped from ServiceTitan detail fields:
Hatch Standard Field | ServiceTitan Detail Field |
---|---|
The value of the first item in the customer:contacts list that has type equal to email | |
External ID | id |
External Contact ID | customer:id |
External Created At | createdOn |
External Updated At (used for Hatch opportunity sorting) |
modifiedOn |
First Name | If there is no comma in customer:name: The first word in the customer:name value If there is a comma in customer:name: All text after the first comma in the customer:name value |
Last Name | If there is no comma in the customer:name value: Everything after the first word in the customer:name value If there is a comma in the customer:name value: All text before the first comma in the customer:name value |
Phone | The value of the first item in the customer:contacts list that has type equal to mobilePhone IF ABOVE IS MISSING: The value of the first item in the customer:contacts list that has type equal to phone |
What are the limitations of the integration?
- In some rare cases, the Estimate record soldBy user may be mapped incorrectly
- This can occur if the soldBy ID maps to an Employee and a Technician (we default to the Technician in this scenario)
- The soldBy ID does not tell us where to attempt to map a user (Employee versus Technician), so we check all possibilities for a match