11ty → Webflow CMS Sync
Use Eleventy as a build-time GraphQL data orchestrator that syncs content to Webflow CMS, enabling a powerful hybrid architecture.
Hybrid Architecture Overview
11ty fetches data via GraphQL at build time, transforms it, and syncs to Webflow CMS. Webflow handles visual presentation while runtime interactions go through middleware.
┌─────────────────────────────────────────────────────────────────────────┐
│ BUILD TIME │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Shopify │ │ 11ty │ │ Webflow CMS │ │
│ │ GraphQL API │─────▶│ Data Layer │─────▶│ (via REST API) │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
│ │ │ │ │
│ │ Transform & │ │
│ │ Map Schema │ │
│ ▼ ▼ ▼ │
│ Products, Collections _data/*.js CMS Collections │
│ Customers, Content templates Published Site │
│ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ RUNTIME │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Webflow │ │ Xano/n8n │ │ Shopify Admin + │ │
│ │ Client JS │─────▶│ Middleware │─────▶│ Google AI │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
│ │
│ User interactions, consent, real-time personalization │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Data Flow Summary
| Phase | Source | Action | Destination |
|---|---|---|---|
| Build-time | Shopify GraphQL | 11ty fetches products | Webflow CMS |
| Build-time | Any GraphQL API | Transform & map data | Webflow CMS items |
| Runtime | User interactions | Client JS → Webhooks | Xano/n8n |
| Runtime | Xano/n8n | Process & route | Shopify Admin / Google AI |
Key Benefits
- ▪ GraphQL at build time - Full query power without client exposure
- ▪ Webflow visual design - Designer-friendly presentation layer
- ▪ SEO-optimized - Static content in Webflow CMS
- ▪ Secure - API tokens never reach the browser
- ▪ Scalable - Sync runs on schedule, not per-request
- ▪ Flexible - Runtime features via middleware layer
Shopify GraphQL Client
11ty data file that fetches all products from Shopify Storefront API with pagination.
const fetch = require('node-fetch');
require('dotenv').config();
const SHOPIFY_DOMAIN = process.env.SHOPIFY_STORE_DOMAIN;
const STOREFRONT_TOKEN = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN;
const API_VERSION = process.env.SHOPIFY_API_VERSION || '2024-01';
const GRAPHQL_ENDPOINT = `https://${SHOPIFY_DOMAIN}/api/${API_VERSION}/graphql.json`;
async function shopifyQuery(query, variables = {}) {
const response = await fetch(GRAPHQL_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token': STOREFRONT_TOKEN,
},
body: JSON.stringify({ query, variables }),
});
const json = await response.json();
if (json.errors) throw new Error(JSON.stringify(json.errors));
return json.data;
}
async function fetchAllProducts() {
const products = [];
let hasNextPage = true;
let cursor = null;
const query = `
query GetProducts($first: Int!, $after: String) {
products(first: $first, after: $after) {
pageInfo { hasNextPage, endCursor }
edges {
node {
id, handle, title, description, descriptionHtml
productType, vendor, tags
priceRange {
minVariantPrice { amount, currencyCode }
}
featuredImage { url, altText }
variants(first: 100) {
edges {
node {
id, title, sku, availableForSale
price { amount, currencyCode }
}
}
}
}
}
}
}
`;
while (hasNextPage) {
const data = await shopifyQuery(query, { first: 250, after: cursor });
products.push(...data.products.edges.map(e => e.node));
hasNextPage = data.products.pageInfo.hasNextPage;
cursor = data.products.pageInfo.endCursor;
}
return products;
}
module.exports = async function() {
const products = await fetchAllProducts();
return { products, fetchedAt: new Date().toISOString() };
};
Products Query with Full Data
Complete GraphQL query for fetching product data with variants, images, and collections.
query GetProducts($first: Int!, $after: String) {
products(first: $first, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
handle
title
description
descriptionHtml
productType
vendor
tags
createdAt
updatedAt
publishedAt
priceRange {
minVariantPrice { amount, currencyCode }
maxVariantPrice { amount, currencyCode }
}
featuredImage { url, altText, width, height }
images(first: 10) {
edges {
node { url, altText, width, height }
}
}
variants(first: 100) {
edges {
node {
id, title, sku, availableForSale
price { amount, currencyCode }
compareAtPrice { amount, currencyCode }
selectedOptions { name, value }
}
}
}
collections(first: 10) {
edges {
node { id, handle, title }
}
}
seo { title, description }
}
}
}
}
User Query with Full Data
Complete GraphQL query for fetching customer/user data with consent status, session history, and metafields.
# User/Customer Query with Full Data
# Requires Shopify Admin API access
query GetCustomer($id: ID!) {
customer(id: $id) {
id
email
firstName
lastName
displayName
phone
createdAt
updatedAt
state
tags
note
verifiedEmail
validEmailAddress
# Consent Status
emailMarketingConsent {
marketingState
marketingOptInLevel
consentUpdatedAt
}
smsMarketingConsent {
marketingState
marketingOptInLevel
consentUpdatedAt
}
# Default Address
defaultAddress {
id
address1
address2
city
province
provinceCode
country
countryCode
zip
phone
}
# All Addresses
addresses(first: 10) {
id
address1
city
country
zip
}
# Order History
orders(first: 10, sortKey: CREATED_AT, reverse: true) {
edges {
node {
id
name
createdAt
totalPriceSet {
shopMoney { amount, currencyCode }
}
fulfillmentStatus
financialStatus
}
}
}
# UCP Metafields (Custom User Data)
metafields(first: 20, namespace: "ucp") {
edges {
node {
id
namespace
key
value
type
}
}
}
}
}
# Mutation: Update Customer with Consent and UCP Data
mutation UpdateCustomerWithUCP($input: CustomerInput!) {
customerUpdate(input: $input) {
customer {
id
email
tags
metafields(first: 20, namespace: "ucp") {
edges {
node { key, value }
}
}
}
userErrors {
field
message
}
}
}
# Variables for Update Mutation
# {
# "input": {
# "id": "gid://shopify/Customer/123456789",
# "tags": ["ucp-user", "analytics-consent", "marketing-consent"],
# "metafields": [
# {
# "namespace": "ucp",
# "key": "consent_status",
# "value": "{\"analytics\":true,\"marketing\":true}",
# "type": "json"
# },
# {
# "namespace": "ucp",
# "key": "session_id",
# "value": "session_123456789",
# "type": "single_line_text_field"
# },
# {
# "namespace": "ucp",
# "key": "last_event",
# "value": "{\"type\":\"page_view\",\"timestamp\":\"2026-01-14T12:00:00Z\"}",
# "type": "json"
# },
# {
# "namespace": "ucp",
# "key": "user_tags",
# "value": "[\"returning-customer\",\"high-engagement\"]",
# "type": "json"
# }
# ]
# }
# }
User Data Fetcher
JavaScript module to fetch and sync user data with Google Session integration.
const fetch = require('node-fetch');
require('dotenv').config();
const SHOPIFY_DOMAIN = process.env.SHOPIFY_STORE_DOMAIN;
const ADMIN_TOKEN = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN;
const API_VERSION = process.env.SHOPIFY_API_VERSION || '2024-01';
const ADMIN_ENDPOINT = `https://${SHOPIFY_DOMAIN}/admin/api/${API_VERSION}/graphql.json`;
async function adminQuery(query, variables = {}) {
const response = await fetch(ADMIN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': ADMIN_TOKEN,
},
body: JSON.stringify({ query, variables }),
});
const json = await response.json();
if (json.errors) throw new Error(JSON.stringify(json.errors));
return json.data;
}
async function fetchCustomerWithUCP(customerId) {
const query = `
query GetCustomerUCP($id: ID!) {
customer(id: $id) {
id
email
firstName
lastName
tags
emailMarketingConsent {
marketingState
consentUpdatedAt
}
metafields(first: 20, namespace: "ucp") {
edges {
node { key, value, type }
}
}
}
}
`;
const data = await adminQuery(query, { id: customerId });
return parseCustomerData(data.customer);
}
function parseCustomerData(customer) {
const metafields = {};
customer.metafields?.edges?.forEach(({ node }) => {
try {
metafields[node.key] = node.type === 'json'
? JSON.parse(node.value)
: node.value;
} catch {
metafields[node.key] = node.value;
}
});
return {
userId: customer.id,
email: customer.email,
name: `${customer.firstName || ''} ${customer.lastName || ''}`.trim(),
tags: customer.tags || [],
consent: {
marketing: customer.emailMarketingConsent?.marketingState === 'SUBSCRIBED',
timestamp: customer.emailMarketingConsent?.consentUpdatedAt,
},
ucp: {
sessionId: metafields.session_id || null,
consentStatus: metafields.consent_status || {},
lastEvent: metafields.last_event || null,
userTags: metafields.user_tags || [],
},
};
}
async function updateCustomerUCP(customerId, ucpData) {
const mutation = `
mutation UpdateCustomerUCP($input: CustomerInput!) {
customerUpdate(input: $input) {
customer { id, tags }
userErrors { field, message }
}
}
`;
const input = {
id: customerId,
tags: ucpData.tags || [],
metafields: [
{
namespace: 'ucp',
key: 'consent_status',
value: JSON.stringify(ucpData.consent || {}),
type: 'json',
},
{
namespace: 'ucp',
key: 'session_id',
value: ucpData.sessionId || '',
type: 'single_line_text_field',
},
{
namespace: 'ucp',
key: 'last_event',
value: JSON.stringify(ucpData.lastEvent || {}),
type: 'json',
},
{
namespace: 'ucp',
key: 'user_tags',
value: JSON.stringify(ucpData.userTags || []),
type: 'json',
},
{
namespace: 'ucp',
key: 'idempotency_key',
value: `${customerId}_${Date.now()}`,
type: 'single_line_text_field',
},
],
};
const data = await adminQuery(mutation, { input });
if (data.customerUpdate.userErrors?.length > 0) {
throw new Error(JSON.stringify(data.customerUpdate.userErrors));
}
return data.customerUpdate.customer;
}
module.exports = {
fetchCustomerWithUCP,
updateCustomerUCP,
parseCustomerData,
adminQuery,
};
Webflow CMS API Client
Rate-limited Webflow API wrapper for creating and updating CMS items.
const fetch = require('node-fetch');
const pLimit = require('p-limit');
require('dotenv').config();
const WEBFLOW_API_TOKEN = process.env.WEBFLOW_API_TOKEN;
const API_BASE = 'https://api.webflow.com/v2';
const limit = pLimit(1);
let lastRequestTime = 0;
async function webflowFetch(endpoint, options = {}) {
return limit(async () => {
// Rate limit: 60 requests/minute
const now = Date.now();
const delay = 350 - (now - lastRequestTime);
if (delay > 0) await new Promise(r => setTimeout(r, delay));
lastRequestTime = Date.now();
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${WEBFLOW_API_TOKEN}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(`Webflow API: ${response.status} - ${JSON.stringify(error)}`);
}
return response.json();
});
}
async function createItem(collectionId, fieldData) {
return webflowFetch(`/collections/${collectionId}/items`, {
method: 'POST',
body: JSON.stringify({
isArchived: false,
isDraft: false,
fieldData: fieldData,
}),
});
}
async function updateItem(collectionId, itemId, fieldData) {
return webflowFetch(`/collections/${collectionId}/items/${itemId}`, {
method: 'PATCH',
body: JSON.stringify({
isArchived: false,
isDraft: false,
fieldData: fieldData,
}),
});
}
async function getCollectionItems(collectionId) {
const items = [];
let offset = 0;
let hasMore = true;
while (hasMore) {
const data = await webflowFetch(
`/collections/${collectionId}/items?offset=${offset}&limit=100`
);
items.push(...(data.items || []));
hasMore = data.items?.length === 100;
offset += 100;
}
return items;
}
module.exports = { createItem, updateItem, getCollectionItems };
Schema Mapper
Transform Shopify product data to Webflow CMS field structure.
function extractShopifyId(gid) {
const match = gid?.match(/\/(\d+)$/);
return match ? match[1] : gid;
}
function mapProductToWebflow(shopifyProduct) {
const { id, handle, title, description, priceRange, featuredImage, variants } = shopifyProduct;
const firstVariant = variants?.edges?.[0]?.node;
return {
// Required Webflow fields
name: title,
slug: handle,
// Custom fields (match your Webflow schema)
'shopify-id': extractShopifyId(id),
'product-handle': handle,
'product-title': title,
'description': description || '',
'price': parseFloat(priceRange?.minVariantPrice?.amount) || 0,
'featured-image': featuredImage?.url || '',
'available': firstVariant?.availableForSale ?? true,
'sku': firstVariant?.sku || '',
'variants-json': JSON.stringify(
variants?.edges?.map(e => ({
id: extractShopifyId(e.node.id),
title: e.node.title,
sku: e.node.sku,
price: e.node.price?.amount,
available: e.node.availableForSale,
})) || []
),
'last-synced': new Date().toISOString(),
};
}
module.exports = { mapProductToWebflow, extractShopifyId };
User Schema Mapper
Transform User data with Google Session and Consent State to Webflow CMS field structure.
/**
* User Schema Mapper
* Maps user data with Google Session and Consent State to Webflow CMS
*/
function generateIdempotencyKey(userId, eventType) {
return `${userId}_${eventType}_${Date.now()}`;
}
function mapUserToWebflow(userData, sessionData, consentData) {
const {
userId,
email,
name,
shopifyCustomerId
} = userData;
const {
sessionId,
deviceInfo,
referrer,
landingPage,
events = []
} = sessionData || {};
const {
analytics = false,
marketing = false,
version = 'v1.0',
timestamp
} = consentData || {};
return {
// Required Webflow fields
name: name || email?.split('@')[0] || 'Anonymous',
slug: userId,
// User Identification
'user-id': userId,
'email': email || '',
'shopify-customer-id': shopifyCustomerId || '',
// Consent Status
'consent-status': JSON.stringify({
analytics: { granted: analytics, timestamp: timestamp || new Date().toISOString() },
marketing: { granted: marketing, timestamp: timestamp || new Date().toISOString() }
}),
'consent-analytics': analytics,
'consent-marketing': marketing,
'consent-timestamp': timestamp || new Date().toISOString(),
'consent-version': version,
// UCP Session / Google Session
'ucp-session-id': sessionId || '',
'ucp-tags': JSON.stringify(generateUserTags(userData, sessionData, consentData)),
'last-event-type': events[events.length - 1]?.type || 'session_start',
'last-event-timestamp': events[events.length - 1]?.timestamp || new Date().toISOString(),
'event-history': JSON.stringify(events.slice(-50)), // Keep last 50 events
// Idempotency Logic
'idempotency-key': generateIdempotencyKey(userId, 'user_sync'),
'last-processed-at': new Date().toISOString(),
'processing-status': 'completed',
'retry-count': 0,
'error-message': '',
// Webhook Management
'webhook-url': process.env.USER_WEBHOOK_URL || '',
'webhook-status': 'active',
'last-webhook-sent': new Date().toISOString(),
'webhook-failures': 0,
// Sync Status
'shopify-synced': !!shopifyCustomerId,
'google-ai-synced': true,
'last-sync-status': 'success',
'last-synced': new Date().toISOString(),
};
}
function generateUserTags(userData, sessionData, consentData) {
const tags = [];
// Device-based tags
if (sessionData?.deviceInfo?.type === 'mobile') tags.push('mobile-user');
if (sessionData?.deviceInfo?.type === 'desktop') tags.push('desktop-user');
// Traffic source tags
if (sessionData?.referrer?.includes('google')) tags.push('organic-traffic');
if (sessionData?.referrer?.includes('facebook')) tags.push('social-traffic');
// Consent tags
if (consentData?.analytics) tags.push('analytics-consent');
if (consentData?.marketing) tags.push('marketing-consent');
// Engagement tags
const eventCount = sessionData?.events?.length || 0;
if (eventCount > 10) tags.push('high-engagement');
if (eventCount > 50) tags.push('power-user');
// Purchase intent tags
const hasCartEvent = sessionData?.events?.some(e => e.type === 'add_to_cart');
if (hasCartEvent) tags.push('cart-active');
return tags;
}
function mapConsentUpdate(userId, consentData) {
return {
'consent-status': JSON.stringify({
analytics: { granted: consentData.analytics, timestamp: new Date().toISOString() },
marketing: { granted: consentData.marketing, timestamp: new Date().toISOString() }
}),
'consent-analytics': consentData.analytics,
'consent-marketing': consentData.marketing,
'consent-timestamp': new Date().toISOString(),
'consent-version': consentData.version || 'v1.0',
'idempotency-key': generateIdempotencyKey(userId, 'consent_update'),
'last-processed-at': new Date().toISOString(),
'last-synced': new Date().toISOString(),
};
}
function mapSessionEvent(userId, event) {
return {
'last-event-type': event.type,
'last-event-timestamp': event.timestamp || new Date().toISOString(),
'idempotency-key': generateIdempotencyKey(userId, event.type),
'last-processed-at': new Date().toISOString(),
};
}
module.exports = {
mapUserToWebflow,
mapConsentUpdate,
mapSessionEvent,
generateIdempotencyKey,
generateUserTags
};
Main Sync Orchestration
Script that fetches from Shopify and syncs to Webflow with incremental updates and ID mapping.
require('dotenv').config();
const { fetchAllProducts } = require('../src/_data/shopify');
const webflow = require('../src/_data/webflow');
const { mapProductToWebflow, extractShopifyId } = require('./schema-mapper');
const COLLECTION_ID = process.env.WEBFLOW_COLLECTION_PRODUCTS;
const DRY_RUN = process.argv.includes('--dry-run');
async function syncProducts() {
console.log('Fetching products from Shopify...');
const products = await fetchAllProducts();
console.log(`Fetched ${products.length} products`);
console.log('Getting existing Webflow items...');
const existingItems = await webflow.getCollectionItems(COLLECTION_ID);
const existingByShopifyId = {};
for (const item of existingItems) {
const sid = item.fieldData['shopify-id'];
if (sid) existingByShopifyId[sid] = item;
}
const stats = { created: 0, updated: 0, skipped: 0 };
for (const product of products) {
const shopifyId = extractShopifyId(product.id);
const mappedData = mapProductToWebflow(product);
const existing = existingByShopifyId[shopifyId];
if (DRY_RUN) {
console.log(`[DRY RUN] Would ${existing ? 'update' : 'create'}: ${product.title}`);
stats.skipped++;
continue;
}
if (existing) {
await webflow.updateItem(COLLECTION_ID, existing.id, mappedData);
console.log(`Updated: ${product.title}`);
stats.updated++;
} else {
await webflow.createItem(COLLECTION_ID, mappedData);
console.log(`Created: ${product.title}`);
stats.created++;
}
}
console.log(`\nSync complete: ${stats.created} created, ${stats.updated} updated, ${stats.skipped} skipped`);
}
syncProducts().catch(console.error);
Environment Configuration
# Shopify Storefront API
SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
SHOPIFY_STOREFRONT_ACCESS_TOKEN=your-storefront-token
SHOPIFY_API_VERSION=2024-01
# Webflow CMS API
WEBFLOW_API_TOKEN=your-webflow-token
WEBFLOW_SITE_ID=your-site-id
WEBFLOW_COLLECTION_PRODUCTS=collection-id-for-products
# Sync Configuration
SYNC_BATCH_SIZE=100
SYNC_RATE_LIMIT_MS=350
NPM Scripts
{
"scripts": {
"build": "eleventy",
"sync": "node scripts/sync-to-webflow.js",
"sync:products": "node scripts/sync-to-webflow.js --collection=products",
"sync:dry-run": "node scripts/sync-to-webflow.js --dry-run",
"dev": "eleventy --serve"
}
}
GitHub Actions Workflow
Automated sync workflow that runs on schedule, manual trigger, or Shopify webhook.
name: Sync Shopify to Webflow
on:
# Scheduled sync (every 6 hours)
schedule:
- cron: '0 */6 * * *'
# Manual trigger
workflow_dispatch:
inputs:
collection:
description: 'Collection to sync (products, collections, or all)'
default: 'all'
publish:
description: 'Publish site after sync'
type: boolean
default: true
# Webhook trigger (from Shopify)
repository_dispatch:
types: [shopify-update]
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run sync
env:
SHOPIFY_STORE_DOMAIN: $
SHOPIFY_STOREFRONT_ACCESS_TOKEN: $
WEBFLOW_API_TOKEN: $
WEBFLOW_SITE_ID: $
WEBFLOW_COLLECTION_PRODUCTS: $
run: |
ARGS=""
if [ "$" == "true" ]; then
ARGS="$ARGS --publish"
fi
npm run sync -- $ARGS
- uses: actions/upload-artifact@v4
with:
name: sync-logs
path: logs/
retention-days: 30
Webflow Collection Schema
Required fields in your Webflow Products collection.
| Field Name | Slug | Type | Required |
|---|---|---|---|
| Name | name | Plain Text | Yes |
| Slug | slug | Plain Text | Yes |
| Shopify ID | shopify-id | Plain Text | Yes |
| Product Title | product-title | Plain Text | No |
| Description | description | Plain Text | No |
| Price | price | Number | No |
| Featured Image | featured-image | Image | No |
| Available | available | Switch | No |
| SKU | sku | Plain Text | No |
| Variants JSON | variants-json | Plain Text | No |
| Last Synced | last-synced | Date/Time | No |
User Collection Schema
Required fields in your Webflow User collection with Consent Status, UCP Tag Events, Idempotency Logic, and Webhook Management.
User Identification
| Field Name | Slug | Type | Required |
|---|---|---|---|
| Name | name | Plain Text | Yes |
| Slug | slug | Plain Text | Yes |
| User ID | user-id | Plain Text | Yes |
| Yes | |||
| Shopify Customer ID | shopify-customer-id | Plain Text | No |
Consent Status
| Field Name | Slug | Type | Description |
|---|---|---|---|
| Consent Status JSON | consent-status | Plain Text | Full consent object as JSON string |
| Analytics Consent | consent-analytics | Switch | User granted analytics tracking |
| Marketing Consent | consent-marketing | Switch | User granted marketing personalization |
| Consent Timestamp | consent-timestamp | Date/Time | Last consent update timestamp |
| Consent Version | consent-version | Plain Text | Version of consent policy accepted |
UCP Tag onChange Events
| Field Name | Slug | Type | onChange Trigger |
|---|---|---|---|
| UCP Session ID | ucp-session-id | Plain Text | Fires webhook on new session |
| UCP Tags | ucp-tags | Plain Text | Fires webhook on tag change |
| Last Event Type | last-event-type | Option | Triggers downstream sync |
| Last Event Timestamp | last-event-timestamp | Date/Time | Used for event ordering |
| Event History JSON | event-history | Plain Text | Appended on each event |
Idempotency Logic
| Field Name | Slug | Type | Purpose |
|---|---|---|---|
| Idempotency Key | idempotency-key | Plain Text | Unique key: {user-id}_{event}_{timestamp} |
| Last Processed At | last-processed-at | Date/Time | Prevents duplicate processing |
| Processing Status | processing-status | Option | pending | processing | completed | failed |
| Retry Count | retry-count | Number | Number of retry attempts |
| Error Message | error-message | Plain Text | Last error for debugging |
Webhook Management
| Field Name | Slug | Type | Description |
|---|---|---|---|
| Webhook URL | webhook-url | Link | Target endpoint for user events |
| Webhook Status | webhook-status | Option | active | paused | failed |
| Last Webhook Sent | last-webhook-sent | Date/Time | Timestamp of last successful send |
| Webhook Secret | webhook-secret | Plain Text | HMAC signing secret (encrypted) |
| Webhook Failures | webhook-failures | Number | Consecutive failure count |
Sync Status
| Field Name | Slug | Type | Description |
|---|---|---|---|
| Shopify Synced | shopify-synced | Switch | User synced to Shopify Customer |
| Google AI Synced | google-ai-synced | Switch | User synced to Google AI Stream |
| Last Sync Status | last-sync-status | Option | success | partial | failed |
| Last Synced | last-synced | Date/Time | Last successful sync timestamp |