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.

Architecture Diagram
┌─────────────────────────────────────────────────────────────────────────┐
│                              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.

JavaScript (src/_data/shopify.js)
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.

GraphQL
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.

GraphQL (Shopify Admin API)
# 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.

JavaScript (src/_data/users.js)
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.

JavaScript (src/_data/webflow.js)
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.

JavaScript (scripts/schema-mapper.js)
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.

JavaScript (scripts/user-schema-mapper.js)
/**
 * 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.

JavaScript (scripts/sync-to-webflow.js)
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

.env.example
# 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

package.json 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.

YAML (.github/workflows/sync-webflow.yml)
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
NamenamePlain TextYes
SlugslugPlain TextYes
Shopify IDshopify-idPlain TextYes
Product Titleproduct-titlePlain TextNo
DescriptiondescriptionPlain TextNo
PricepriceNumberNo
Featured Imagefeatured-imageImageNo
AvailableavailableSwitchNo
SKUskuPlain TextNo
Variants JSONvariants-jsonPlain TextNo
Last Syncedlast-syncedDate/TimeNo

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
NamenamePlain TextYes
SlugslugPlain TextYes
User IDuser-idPlain TextYes
EmailemailEmailYes
Shopify Customer IDshopify-customer-idPlain TextNo

Consent Status

Field Name Slug Type Description
Consent Status JSONconsent-statusPlain TextFull consent object as JSON string
Analytics Consentconsent-analyticsSwitchUser granted analytics tracking
Marketing Consentconsent-marketingSwitchUser granted marketing personalization
Consent Timestampconsent-timestampDate/TimeLast consent update timestamp
Consent Versionconsent-versionPlain TextVersion of consent policy accepted

UCP Tag onChange Events

Field Name Slug Type onChange Trigger
UCP Session IDucp-session-idPlain TextFires webhook on new session
UCP Tagsucp-tagsPlain TextFires webhook on tag change
Last Event Typelast-event-typeOptionTriggers downstream sync
Last Event Timestamplast-event-timestampDate/TimeUsed for event ordering
Event History JSONevent-historyPlain TextAppended on each event

Idempotency Logic

Field Name Slug Type Purpose
Idempotency Keyidempotency-keyPlain TextUnique key: {user-id}_{event}_{timestamp}
Last Processed Atlast-processed-atDate/TimePrevents duplicate processing
Processing Statusprocessing-statusOptionpending | processing | completed | failed
Retry Countretry-countNumberNumber of retry attempts
Error Messageerror-messagePlain TextLast error for debugging

Webhook Management

Field Name Slug Type Description
Webhook URLwebhook-urlLinkTarget endpoint for user events
Webhook Statuswebhook-statusOptionactive | paused | failed
Last Webhook Sentlast-webhook-sentDate/TimeTimestamp of last successful send
Webhook Secretwebhook-secretPlain TextHMAC signing secret (encrypted)
Webhook Failureswebhook-failuresNumberConsecutive failure count

Sync Status

Field Name Slug Type Description
Shopify Syncedshopify-syncedSwitchUser synced to Shopify Customer
Google AI Syncedgoogle-ai-syncedSwitchUser synced to Google AI Stream
Last Sync Statuslast-sync-statusOptionsuccess | partial | failed
Last Syncedlast-syncedDate/TimeLast successful sync timestamp