How to Connect Shopify to HubSpot (Automated Data Sync)
📊 Integration Overview This integration blueprint outlines a robust, event-driven pipeline for synchronizing customer data from Shopify to HubSpot in real-time. Utilizing Shopify webhooks as the trigger, the system captures new customer registrations or updates. The incoming payload is then transformed to align with HubSpot's CRM contact schema and pushed to the appropriate HubSpot API endpoint. This ensures that sales and marketing teams leveraging HubSpot always have the most current customer information, facilitating personalized campaigns, improved customer segmentation, and consistent customer experiences by avoiding stale data. The pipeline incorporates comprehensive error handling, including rate limit management, duplicate data prevention, and robust authentication, to maintain data integrity and system reliability.
Available integrations in directory: Shopify to QuickBooks, Shopify to Xero, Stripe to HubSpot.
🛠️ Core Connection Requirements
Primary Key: email (for customer identification in HubSpot)
Trigger Event: customer/create, customer/update (Shopify)
Action Event: create/update contact, create/update company (HubSpot)
📋 The 5-Step Execution Blueprint
Step 1: Authentication & Scope Configuration Securely authenticate with both Shopify and HubSpot, ensuring the necessary API access permissions are granted.
Shopify (Trigger App):
- Authentication Method: Shopify Admin API Access Token or OAuth 2.0. For server-side integrations, a private app's Admin API Access Token is often preferred for its simplicity and persistence.
- Required Scopes:
read_customers,write_customers,read_orders,write_webhooks,read_webhook_subscriptions. Theread_customersscope is crucial for fetching customer details, whilewrite_webhooksis needed to register the trigger.
HubSpot (Action App):
- Authentication Method: Private App Access Token or OAuth 2.0. Private Apps provide a simple, robust authentication method with granular scope control for server-to-server communication.
- Required Scopes:
crm.objects.contacts.read,crm.objects.contacts.write,crm.objects.companies.read,crm.objects.companies.write. These scopes permit reading and writing contact and company data in the CRM.
Sample .env setup:
SHOPIFY_API_KEY=shpat_YOUR_SHOPIFY_API_KEY
SHOPIFY_API_SECRET=shpss_YOUR_SHOPIFY_API_SECRET
SHOPIFY_STORE_DOMAIN=your-store-name.myshopify.com
SHOPIFY_WEBHOOK_SECRET=YOUR_SECURE_WEBHOOK_SECRET
HUBSPOT_API_KEY=pat_YOUR_HUBSPOT_PRIVATE_APP_TOKEN
HUBSPOT_APP_ID=YOUR_HUBSPOT_APP_ID # If using OAuth, otherwise optional
HUBSPOT_OAUTH_CLIENT_ID=YOUR_HUBSPOT_OAUTH_CLIENT_ID # If using OAuth
HUBSPOT_OAUTH_CLIENT_SECRET=YOUR_HUBSPOT_OAUTH_CLIENT_SECRET # If using OAuth
INTEGRATION_WEBHOOK_URL=https://your-integration-service.com/webhooks/shopify
Step 2: Webhook Trigger Setup
Register a webhook subscription in Shopify for customer/create and customer/update events, pointing to your integration service's public endpoint. Implement robust signature validation to verify the authenticity of incoming requests.
import express from 'express';
import crypto from 'crypto';
import bodyParser from 'body-parser';
const app = express();
const SHOPIFY_WEBHOOK_SECRET = process.env.SHOPIFY_WEBHOOK_SECRET || ''; // Ensure this is loaded from .env
// Raw body parser for webhook signature validation
app.use(bodyParser.json({
verify: (req, res, buf) => {
(req as any).rawBody = buf;
}
}));
app.post('/webhooks/shopify', (req, res) => {
const hmacHeader = req.get('X-Shopify-Hmac-Sha256');
const body = (req as any).rawBody;
// Validate webhook signature
const digest = crypto
.createHmac('sha256', SHOPIFY_WEBHOOK_SECRET)
.update(body, 'utf8')
.digest('base64');
if (digest !== hmacHeader) {
console.error('Webhook signature validation failed!');
return res.status(401).send('Not Authorized');
}
// Webhook is legitimate, process payload
const topic = req.get('X-Shopify-Topic');
const shopDomain = req.get('X-Shopify-Shop-Domain');
const customerData = req.body;
console.log(`Received Shopify webhook: ${topic} from ${shopDomain}`);
console.log('Customer data:', customerData);
// Enqueue customerData for processing (e.g., to a BullMQ queue)
// processCustomerWebhook(customerData, topic);
res.status(200).send('Webhook received and processed');
});
// Example of how to register a webhook (run once or as part of deployment)
async function registerShopifyWebhook(shopifyDomain: string, shopifyApiKey: string, webhookUrl: string, topic: string) {
const url = `https://${shopifyDomain}/admin/api/2023-10/webhooks.json`; // Adjust API version as needed
const headers = {
'X-Shopify-Access-Token': shopifyApiKey,
'Content-Type': 'application/json',
};
const payload = {
webhook: {
topic: topic,
address: webhookUrl,
format: 'json',
secret: SHOPIFY_WEBHOOK_SECRET, // It's good practice to set this programmatically if your API allows
},
};
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload),
});
const data = await response.json();
if (response.ok) {
console.log(`Webhook for topic '${topic}' registered successfully:`, data);
} else {
console.error(`Failed to register webhook for topic '${topic}':`, data);
}
} catch (error) {
console.error('Error registering Shopify webhook:', error);
}
}
// Example usage (call once):
// registerShopifyWebhook(process.env.SHOPIFY_STORE_DOMAIN, process.env.SHOPIFY_API_KEY, process.env.INTEGRATION_WEBHOOK_URL, 'customers/create');
// registerShopifyWebhook(process.env.SHOPIFY_STORE_DOMAIN, process.env.SHOPIFY_API_KEY, process.env.INTEGRATION_WEBHOOK_URL, 'customers/update');
// app.listen(3000, () => console.log('Webhook server listening on port 3000'));
Step 3: Payload Transformation & Mapping Map the incoming Shopify customer JSON payload to HubSpot's contact properties. This ensures data consistency and proper attribution within the CRM.
Sample Input (Shopify Customer Webhook Payload - customer/create):
{
"id": 6072173549642,
"email": "john.doe@example.com",
"accepts_marketing": true,
"created_at": "2023-01-15T10:00:00-05:00",
"updated_at": "2023-01-15T10:00:00-05:00",
"first_name": "John",
"last_name": "Doe",
"orders_count": 0,
"state": "enabled",
"total_spent": "0.00",
"last_order_id": null,
"note": null,
"verified_email": true,
"multipass_identifier": null,
"tax_exempt": false,
"phone": "+15551234567",
"tags": "New Customer",
"last_order_name": null,
"currency": "USD",
"addresses": [
{
"id": 7234567890123,
"customer_id": 6072173549642,
"first_name": "John",
"last_name": "Doe",
"company": null,
"address1": "123 Main St",
"address2": null,
"city": "Anytown",
"province": "NY",
"country": "United States",
"zip": "12345",
"phone": "+15551234567",
"name": "John Doe",
"province_code": "NY",
"country_code": "US",
"default": true
}
],
"admin_graphql_api_id": "gid://shopify/Customer/6072173549642"
}
Sample Output (HubSpot Contact Payload - POST /crm/v3/objects/contacts):
{
"properties": {
"email": "john.doe@example.com",
"firstname": "John",
"lastname": "Doe",
"phone": "+15551234567",
"address": "123 Main St",
"city": "Anytown",
"state": "NY",
"zip": "12345",
"country": "United States",
"shopify_customer_id": "6072173549642",
"accepts_marketing": true,
"source_app": "Shopify",
"tags": "New Customer"
}
}
Mapping Logic:
shopify_customer_id(custom property in HubSpot) ->idemail->emailfirst_name->firstnamelast_name->lastnamephone->phoneaddresses[0].address1->addressaddresses[0].city->cityaddresses[0].province_code->stateaddresses[0].zip->zipaddresses[0].country->countryaccepts_marketing->accepts_marketing(custom boolean property in HubSpot)tags->tags(custom multi-select/text property in HubSpot)source_app(custom property) -> "Shopify"
Step 4: Endpoint Despatch & Error Guarding Dispatch the transformed payload to HubSpot's CRM API. Implement robust error handling for common API issues, ensuring data integrity and system resilience.
import axios from 'axios';
import { Redis } from 'ioredis';
import { Queue } from 'bullmq';
const HUBSPOT_API_KEY = process.env.HUBSPOT_API_KEY || '';
const redis = new Redis(); // Connect to your Redis instance
const contactQueue = new Queue('hubspot-contact-sync', { connection: redis });
async function syncCustomerToHubSpot(customerPayload: any) {
const hubspotContact = transformShopifyToHubSpot(customerPayload);
const contactEmail = hubspotContact.properties.email;
try {
// 1. Check for existing contact by email (idempotency)
let hubspotContactId = null;
try {
const searchResponse = await axios.post(
'https://api.hubapi.com/crm/v3/objects/contacts/search',
{
filterGroups: [
{
filters: [
{
propertyName: 'email',
operator: 'EQ',
value: contactEmail,
},
],
},
],
properties: ['email', 'firstname', 'lastname'], // Fetch relevant properties
},
{ headers: { Authorization: `Bearer ${HUBSPOT_API_KEY}` } }
);
if (searchResponse.data.results.length > 0) {
hubspotContactId = searchResponse.data.results[0].id;
console.log(`Contact with email ${contactEmail} already exists. ID: ${hubspotContactId}`);
}
} catch (searchError) {
console.warn(`Error searching for existing contact ${contactEmail}:`, (searchError as any).response?.data || searchError.message);
// Proceed to create if search fails or no contact found
}
// 2. Create or Update Contact
if (hubspotContactId) {
// Update existing contact
const updateResponse = await axios.patch(
`https://api.hubapi.com/crm/v3/objects/contacts/${hubspotContactId}`,
hubspotContact,
{ headers: { Authorization: `Bearer ${HUBSPOT_API_KEY}` } }
);
console.log('HubSpot contact updated successfully:', updateResponse.data);
} else {
// Create new contact
const createResponse = await axios.post(
'https://api.hubapi.com/crm/v3/objects/contacts',
hubspotContact,
{ headers: { Authorization: `Bearer ${HUBSPOT_API_KEY}` } }
);
console.log('HubSpot contact created successfully:', createResponse.data);
}
} catch (error) {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const errorMessage = error.response?.data || error.message;
switch (status) {
case 401:
console.error(`HubSpot API 401: Unauthorized. Token might be expired or invalid. Attempting token refresh/re-authentication for ${contactEmail}.`);
// Implement token refresh logic here if using OAuth. For private apps, manual intervention may be needed.
await contactQueue.add('retry-contact-sync', customerPayload, { delay: 60 * 1000, attempts: 3 }); // Retry after 1 minute
break;
case 400:
console.error(`HubSpot API 400: Bad Request for ${contactEmail}. Payload validation failed. Error: ${JSON.stringify(errorMessage)}`);
// Log detailed error and potentially send alert. Do not retry automatically unless mapping can be fixed.
break;
case 429:
console.warn(`HubSpot API 429: Rate limit exceeded for ${contactEmail}. Adding to queue with exponential backoff.`);
// Add job to a queue (e.g., BullMQ) with a delay and exponential backoff
await contactQueue.add('retry-contact-sync', customerPayload, { delay: 5 * 1000, attempts: 5, backoff: { type: 'exponential', delay: 1000 } });
break;
case 500:
case 502:
case 503:
case 504:
console.error(`HubSpot API ${status}: Server Error for ${contactEmail}. Retrying with backoff.`);
await contactQueue.add('retry-contact-sync', customerPayload, { delay: 15 * 1000, attempts: 5, backoff: { type: 'exponential', delay: 2000 } });
break;
default:
console.error(`HubSpot API Error ${status} for ${contactEmail}: ${JSON.stringify(errorMessage)}`);
// Log and alert for unhandled errors
break;
}
} else {
console.error(`An unexpected error occurred during HubSpot sync for ${contactEmail}:`, error);
}
}
}
// Dummy transform function
function transformShopifyToHubSpot(shopifyCustomer: any) {
const address = shopifyCustomer.addresses?.[0];
return {
properties: {
email: shopifyCustomer.email,
firstname: shopifyCustomer.first_name,
lastname: shopifyCustomer.last_name,
phone: shopifyCustomer.phone,
address: address?.address1,
city: address?.city,
state: address?.province_code,
zip: address?.zip,
country: address?.country,
shopify_customer_id: String(shopifyCustomer.id), // Store as string to avoid potential BigInt issues
accepts_marketing: shopifyCustomer.accepts_marketing,
source_app: "Shopify",
tags: shopifyCustomer.tags
}
};
}
// Example usage within your webhook handler after validation and parsing:
// processCustomerWebhook function (from Step 2) would call:
// syncCustomerToHubSpot(customerData);
Step 5: Live Loop Validation Thoroughly test the integration in sandbox environments. Create/update customer records in Shopify's development store and verify the corresponding contact creation/updates in HubSpot's developer portal.
- Sandbox Environment Setup:
- Set up a Shopify Development Store.
- Set up a HubSpot Developer Account or Sandbox Account.
- Configure your integration service to point to these sandbox environments using dedicated API keys/tokens.
- Test Cases:
- New Customer Creation: Create a new customer in Shopify and verify a new contact is created in HubSpot with all mapped properties correctly populated (e.g., email, first name, last name, address,
shopify_customer_id). - Customer Update: Update an existing customer's details (e.g., phone number, address,
accepts_marketingstatus) in Shopify and verify that the corresponding contact in HubSpot is updated, specifically checking for no duplication or truncation of existing data. - Edge Cases:
- Customer with missing data (e.g., no phone number, no address).
- Customer with special characters in names/addresses.
- High volume of customer creations/updates to stress-test rate limit handling.
- New Customer Creation: Create a new customer in Shopify and verify a new contact is created in HubSpot with all mapped properties correctly populated (e.g., email, first name, last name, address,
- Validation Queries:
- HubSpot: Navigate to
Contactsin your HubSpot sandbox. Search for the customer's email orshopify_customer_id(if mapped as a custom property). Inspect the contact record's properties tab to ensure all data points are accurately transferred. - Shopify: After updating a customer, ensure that the webhook event fired successfully. Monitor your integration service logs for successful payload processing and HubSpot API responses.
- Database/Queue Monitoring: If using a queue (e.g., Redis/BullMQ), monitor its dashboard to ensure jobs are processed without errors, retries are handled correctly, and the queue backlog doesn't grow indefinitely.
- HubSpot: Navigate to
Ensure to check for successful HTTP 2xx responses from HubSpot's API and verify data integrity by comparing specific field values between Shopify and HubSpot.
❓ Integration Frequently Asked Questions
Q: How does this pipeline handle duplicate data entries?
A: The pipeline prevents duplicate contact entries in HubSpot by first attempting to search for an existing contact using the customer's email (the primary key). Before dispatching a POST request to create a new contact, a POST /crm/v3/objects/contacts/search request is made. If a contact with the matching email is found, the system performs an PATCH request to update the existing contact's record (using its HubSpot id) instead of creating a new one. This idempotent behavior ensures that customer/update events for existing customers correctly modify their profiles without generating duplicates. The shopify_customer_id custom property can also be used as a secondary unique identifier for robust cross-referencing.
Q: What happens if the API rate limit is exceeded during high volume? A: To manage HubSpot's API rate limits (e.g., 100 requests per 10 seconds per API key), the integration employs an asynchronous processing queue (e.g., using BullMQ with Redis). When a HubSpot API request returns a 429 (Too Many Requests) HTTP status code, the current job (customer sync request) is not immediately failed. Instead, it is re-enqueued with a delay using an exponential backoff strategy. This means subsequent retries will have progressively longer delays (e.g., 5s, 10s, 20s, 40s), preventing continuous hitting of the rate limit while ensuring eventual data delivery. A dedicated worker consumes jobs from this queue at a controlled rate, dynamically adjusting its processing speed based on API responses. Monitoring and alerting are also in place to notify administrators if the queue backlog grows excessively, indicating persistent rate limit issues.