How to Connect Shopify to Klaviyo (Automated Data Sync)
📊 Integration Overview This integration blueprint outlines a robust, real-time data synchronization pipeline between Shopify, a leading e-commerce platform, and Klaviyo, a powerful marketing automation and email platform. The core objective is to capture critical customer and order events from Shopify as they occur and push them into Klaviyo to enrich customer profiles, trigger marketing flows, and enable highly targeted segmentation. This ensures that marketing efforts in Klaviyo are always based on the most current customer behavior and purchase data, leading to improved customer engagement and conversion rates. The data flow typically involves Shopify sending webhook notifications for events like new customer sign-ups or completed orders, which are then received by an intermediary service, transformed, and subsequently sent to Klaviyo's API to create or update customer profiles and track custom events.
Available integrations in directory: Connect Shopify to HubSpot, Connect Shopify to QuickBooks, Connect Shopify to Salesforce, Connect Shopify to Wave Accounting, Connect Shopify to Xero, Connect Shopify to Zoho CRM.
🛠️ Core Connection Requirements
Primary Key: email (for customers), order_id (for orders)
Trigger Event: customer_created, order_created, order_updated in Shopify
Action Event: create_profile, update_profile, track_event in Klaviyo
📋 The 5-Step Execution Blueprint
Step 1: Authentication & Scope Configuration To securely connect Shopify and Klaviyo, you'll need API credentials for both platforms.
Shopify:
A private app or a custom app must be created in your Shopify Admin (Apps > Develop apps). This provides an Admin API Access Token with specific permissions (scopes).
Required Scopes:
read_customersread_orders
Klaviyo:
A Private API Key is required for full access to Klaviyo's API for creating/updating profiles and tracking events. This can be found in your Klaviyo account settings (Account > Settings > API Keys).
Environment Variable Setup: For secure handling of credentials, use environment variables.
# .env
SHOPIFY_API_KEY="shpat_YOUR_SHOPIFY_ADMIN_API_ACCESS_TOKEN"
SHOPIFY_WEBHOOK_SECRET="YOUR_SHOPIFY_WEBHOOK_SECRET_KEY"
KLAVIYO_PRIVATE_API_KEY="pk_YOUR_KLAVIYO_PRIVATE_API_KEY"
Step 2: Webhook Trigger Setup Shopify webhooks will notify your integration endpoint in real-time about specified events. You need to set up webhook subscriptions in your Shopify store admin or programmatically via the Shopify Admin API.
Webhook Registration (Manual or Programmatic):
Register webhooks for customers/create, orders/create, and orders/updated pointing to your integration's public endpoint (e.g., https://your-integration.com/webhooks/shopify).
Webhook Endpoint Verification (TypeScript/JavaScript):
Shopify sends a X-Shopify-Hmac-Sha256 header with each webhook request. You must validate this signature to ensure the request originated from Shopify and hasn't been tampered with.
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!;
app.post('/webhooks/shopify', bodyParser.raw({ type: 'application/json' }), (req, res) => {
const hmacHeader = req.get('X-Shopify-Hmac-Sha256');
const body = req.body.toString(); // Raw body as buffer
const digest = crypto
.createHmac('sha256', SHOPIFY_WEBHOOK_SECRET)
.update(body)
.digest('base64');
if (digest === hmacHeader) {
console.log('Webhook signature verified successfully.');
const data = JSON.parse(body);
const topic = req.get('X-Shopify-Topic');
if (topic === 'customers/create') {
console.log('New customer created:', data.email);
// Process new customer data
} else if (topic === 'orders/create' || topic === 'orders/updated') {
console.log('Order event received:', data.order_number);
// Process order data
}
res.status(200).send('Webhook received and verified');
} else {
console.error('Webhook signature verification failed!');
res.status(401).send('Unauthorized');
}
});
app.listen(3000, () => console.log('Webhook listener running on port 3000'));
Step 3: Payload Transformation & Mapping Once a Shopify webhook is received and verified, the raw payload needs to be transformed into a format compatible with Klaviyo's API. This involves mapping Shopify fields to Klaviyo profile properties and event metrics.
Sample Shopify Customer Webhook Input:
{
"id": 123456789,
"email": "customer@example.com",
"first_name": "Jane",
"last_name": "Doe",
"phone": "+15551234567",
"verified_email": true,
"state": "enabled",
"addresses": [
{
"address1": "123 Main St",
"city": "Anytown",
"province": "CA",
"zip": "90210",
"country": "United States"
}
],
"created_at": "2023-01-01T10:00:00-05:00",
"updated_at": "2023-01-01T10:00:00-05:00"
}
Klaviyo Profile Output (for customer_created or customer_updated):
This payload will be sent to POST /api/profiles/ (or POST /api/identify for older APIs).
{
"data": {
"type": "profile",
"attributes": {
"email": "customer@example.com",
"phone_number": "+15551234567",
"first_name": "Jane",
"last_name": "Doe",
"organization": null,
"title": null,
"image": null,
"properties": {
"Shopify ID": 123456789,
"Verified Email": true,
"Customer State": "enabled",
"Address1": "123 Main St",
"City": "Anytown",
"Region": "CA",
"Zip": "90210",
"Country": "United States"
}
}
}
}
Sample Shopify Order Webhook Input:
{
"id": 987654321,
"order_number": "#1001",
"email": "customer@example.com",
"created_at": "2023-01-01T10:30:00-05:00",
"total_price": "99.99",
"currency": "USD",
"line_items": [
{
"id": 111,
"title": "Product A",
"price": "49.99",
"quantity": 1,
"sku": "PA123"
},
{
"id": 222,
"title": "Product B",
"price": "50.00",
"quantity": 1,
"sku": "PB456"
}
],
"shipping_address": {
"address1": "123 Main St",
"city": "Anytown",
"province": "CA",
"zip": "90210",
"country": "United States"
}
}
Klaviyo Event Output (for order_created):
This payload will be sent to POST /api/events/.
{
"data": {
"type": "event",
"attributes": {
"profile": {
"data": {
"type": "profile",
"attributes": {
"email": "customer@example.com"
}
}
},
"metric": {
"data": {
"type": "metric",
"attributes": {
"name": "Ordered Product"
}
}
},
"properties": {
"OrderId": 987654321,
"OrderNumber": "#1001",
"Value": 99.99,
"Currency": "USD",
"Items": [
{
"SKU": "PA123",
"Name": "Product A",
"Price": 49.99,
"Quantity": 1
},
{
"SKU": "PB456",
"Name": "Product B",
"Price": 50.00,
"Quantity": 1
}
],
"ShippingAddress": "123 Main St, Anytown, CA 90210, United States"
},
"time": "2023-01-01T15:30:00Z"
}
}
}
Step 4: Endpoint Despatch & Error Guarding After transforming the payload, make the API request to Klaviyo. Robust error handling is crucial for a reliable integration.
Klaviyo API Requests (using axios):
import axios from 'axios';
const KLAVIYO_PRIVATE_API_KEY = process.env.KLAVIYO_PRIVATE_API_KEY!;
const KLAVIYO_API_URL = 'https://a.klaviyo.com/api'; // V2 API endpoint. For V3/V4, it's https://a.klaviyo.com/api/v2 or v3, etc.
async function sendToKlaviyo(endpoint: string, payload: any) {
try {
const headers = {
'Authorization': `Klaviyo-API-Key ${KLAVIYO_PRIVATE_API_KEY}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
'Revision': '2023-02-22' // Specify API revision for V3/V4
};
const response = await axios.post(`${KLAVIYO_API_URL}${endpoint}`, payload, { headers });
console.log(`Successfully sent data to Klaviyo ${endpoint}:`, response.status);
return response.data;
} catch (error: any) {
if (error.response) {
console.error(`Klaviyo API Error (${error.response.status}):`, error.response.data);
throw new Error(`Klaviyo API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
} else if (error.request) {
console.error('Klaviyo API No response received:', error.request);
throw new Error('Klaviyo API No response received');
} else {
console.error('Klaviyo API Request setup error:', error.message);
throw new Error(`Klaviyo API Request setup error: ${error.message}`);
}
}
}
// Example usage:
// await sendToKlaviyo('/api/profiles/', klaviyoProfilePayload); // For V3+ API
// await sendToKlaviyo('/api/events/', klaviyoEventPayload); // For V3+ API
// For V2: await sendToKlaviyo('/api/identify', {token: KLAVIYO_PUBLIC_API_KEY, properties: klaviyoProfileProperties});
// For V2: await sendToKlaviyo('/api/track', {token: KLAVIYO_PUBLIC_API_KEY, event: "Ordered Product", properties: klaviyoEventProperties});
Error Guarding Strategies:
-
401 Unauthorized:
- Cause: Invalid or missing
KLAVIYO_PRIVATE_API_KEY. - Handling: Log the error immediately. This is usually a configuration issue. For integrations using OAuth, it would typically trigger a token refresh flow. For Klaviyo's private key, manual intervention to update the key is required.
- Cause: Invalid or missing
-
400 Bad Request:
- Cause: The request body is malformed, missing required fields, or has invalid data types according to Klaviyo's API schema.
- Handling: Log the specific error message from Klaviyo's response body, which usually indicates the exact field causing the issue. This often requires reviewing the payload transformation logic in Step 3. Implement robust schema validation before sending the request.
-
429 Rate Limiting:
- Cause: Exceeding the number of API requests allowed within a specific timeframe (e.g., 500 requests per second for V3+).
- Handling:
- Asynchronous Queueing: Implement a message queue system (e.g., BullMQ with Redis) to buffer incoming requests during high-volume periods. When a rate limit is hit, instead of retrying immediately, add the task to the queue.
- Exponential Backoff with Jitter: When retrying failed requests (including 429), use an exponential backoff strategy. Wait for
(2^n - 1)seconds (wherenis the number of retries), adding a small random "jitter" to prevent stampeding. The queue worker should implement this.
-
5xx Server Errors (500, 502, 503, 504):
- Cause: Temporary issues on Klaviyo's server, network problems, or gateway timeouts.
- Handling: Implement a retry mechanism with exponential backoff. These errors are often transient, so retrying after a short delay is usually effective. If retries continue to fail after a defined number (e.g., 5 attempts), log the error and potentially move the task to a dead-letter queue for manual investigation.
Step 5: Live Loop Validation Thorough testing is paramount to ensure the integration functions correctly, consistently, and without data integrity issues.
Testing Workflow:
- Sandbox Environments:
- Shopify Development Store: Use a Shopify development store or a staging environment where live customer data is not affected.
- Klaviyo Test Account: Dedicate a separate Klaviyo account or a list/segment in your main account specifically for testing, preventing test data from polluting production metrics or engaging real customers.
- Trigger Test Events:
- Customer Creation: In Shopify, create a new test customer account.
- Order Placement: Place a test order using the newly created customer or an existing test customer.
- Order Updates: Change the status of a test order (e.g., fulfill it) to trigger
orders/updatedwebhooks.
- Validation Queries (Klaviyo UI/API):
- Profile Verification: Log into your Klaviyo test account. Navigate to
Audience > Profilesand search for the email address of your test customer.- Verify Existence: Confirm the profile exists.
- Verify Properties: Check if all expected custom properties (e.g., "Shopify ID", "Verified Email") are correctly mapped and populated without truncation.
- Event Verification: In the test customer's profile, review the "Activity Feed" to ensure
Ordered ProductorPlaced Orderevents appear.- Verify Event Data: Click on the event to inspect its properties (e.g.,
OrderId,Value,Items). Confirm that the data matches the Shopify source and there's no duplication of events for the same order.
- Verify Event Data: Click on the event to inspect its properties (e.g.,
- Profile Verification: Log into your Klaviyo test account. Navigate to
- Error Log Monitoring:
- Continuously monitor the logs of your integration service for any errors (4xx, 5xx) during the testing phase. Investigate and resolve any discrepancies.
- Ensure the queueing system (if implemented) is processing tasks and not accumulating dead letters without reason.
❓ Integration Frequently Asked Questions
Q: How does this pipeline handle duplicate data entries?
A: The pipeline prevents duplicate data entries primarily by leveraging Klaviyo's inherent idempotency for profile and event tracking, combined with explicit checks where necessary. For customer profiles, Klaviyo uses the email address as the primary unique identifier. When you send a profile update or creation request with an existing email, Klaviyo intelligently updates the existing profile instead of creating a new one. To explicitly handle potential race conditions or ensure data integrity, our integration first attempts to GET a profile from Klaviyo using the customer's email. If a profile with that email exists, we proceed with an update (PUT or PATCH operation, or simply a POST with the V3 API which handles updates). If no profile is found, we then perform a POST to create a new profile. For events (e.g., "Ordered Product"), Klaviyo also handles some level of idempotency if an EventId is provided (though less strict than profile unique IDs). Our integration includes a unique identifier for each order (the Shopify order_id) within the event properties, allowing for easy identification and filtering of potential duplicate events in Klaviyo reports, though Klaviyo's V3+ API is designed to deduplicate events that are identical in metric, profile, properties, and time if sent very close together.
Q: What happens if the API rate limit is exceeded during high volume?
A: Exceeding API rate limits is a common challenge during high-volume data synchronization. Our blueprint addresses this with a robust asynchronous processing mechanism using a message queue system, typically BullMQ backed by Redis. When an incoming Shopify webhook is received, instead of making an immediate synchronous call to Klaviyo, the processed payload is enqueued as a job in Redis. A separate set of worker processes then picks up these jobs from the queue. Each worker is configured with a built-in exponential backoff and retry logic. If a worker attempts to send data to Klaviyo and receives a 429 Too Many Requests response, it pauses, recalculates a progressively longer delay (e.g., 1s, 2s, 4s, 8s, up to a configured maximum), and then retries the job. This strategy, often combined with a small random jitter to prevent all workers from retrying simultaneously, effectively throttles requests and prevents overwhelming Klaviyo's API. This ensures that all data eventually gets processed without loss, even during peak load, maintaining the real-time nature of the sync while respecting API limitations.