Data Layer Setup
Pendo's data layer consists of visitor (user) and account (organization) metadata that enriches your analytics and enables powerful segmentation, targeting, and reporting capabilities.
Visitor Data Structure
Visitor data represents individual users in your application. The visitor.id is the only required field, but additional metadata unlocks Pendo's full potential.
Required Fields
pendo.initialize({
visitor: {
id: 'USER_ID' // Required: Unique identifier for the user
}
});
Recommended Fields
pendo.initialize({
visitor: {
id: 'user-12345', // Required: Unique user ID
email: 'user@example.com', // Recommended: User email
full_name: 'John Doe', // Recommended: Display name
role: 'admin' // Recommended: User role/type
}
});
Comprehensive Visitor Data
pendo.initialize({
visitor: {
// Identity
id: 'user-12345',
email: 'john.doe@example.com',
username: 'johndoe',
full_name: 'John Doe',
first_name: 'John',
last_name: 'Doe',
// Profile
role: 'admin',
title: 'Product Manager',
department: 'Product',
team: 'Growth',
// Account relationship
account_id: 'acct-789',
is_account_owner: true,
permissions: 'admin,billing,analytics',
// Subscription
plan_type: 'enterprise',
subscription_status: 'active',
trial_user: false,
// Activity
signup_date: '2024-01-15',
last_login: '2024-12-26',
login_count: 150,
// Engagement
feature_flags: 'new_dashboard,beta_reports',
onboarding_completed: true,
onboarding_step: 5,
// Custom fields
company_size: '100-500',
industry: 'SaaS',
use_case: 'Product Analytics'
}
});
Account Data Structure
Account data represents organizations, companies, or tenants in multi-tenant applications.
Basic Account Data
pendo.initialize({
visitor: {
id: 'user-12345'
},
account: {
id: 'acct-789', // Required: Unique account ID
name: 'Acme Corporation' // Recommended: Account name
}
});
Comprehensive Account Data
pendo.initialize({
visitor: {
id: 'user-12345'
},
account: {
// Identity
id: 'acct-789',
name: 'Acme Corporation',
domain: 'acme.com',
// Subscription
plan: 'enterprise',
plan_tier: 'premium',
subscription_status: 'active',
billing_cycle: 'annual',
// Revenue
mrr: 2500, // Monthly recurring revenue
arr: 30000, // Annual recurring revenue
ltv: 75000, // Lifetime value
total_spend: 45000,
// Company details
industry: 'Technology',
company_size: '100-500',
number_of_employees: 250,
founded_year: 2015,
// Location
country: 'United States',
state: 'California',
city: 'San Francisco',
timezone: 'America/Los_Angeles',
// Engagement
signup_date: '2023-06-01',
contract_start: '2024-01-01',
contract_end: '2025-01-01',
renewal_date: '2025-01-01',
// Usage
total_users: 50,
active_users: 35,
seats_purchased: 75,
storage_used_gb: 1500,
// Features
enabled_features: 'api,sso,custom_reports',
integration_count: 12,
// Support
support_tier: 'premium',
account_manager: 'Jane Smith',
health_score: 85,
// Custom fields
lead_source: 'Referral',
customer_segment: 'Enterprise',
product_interest: 'Analytics'
}
});
Data Types and Formatting
Supported Data Types
pendo.initialize({
visitor: {
id: 'user-123',
// Strings
email: 'user@example.com',
role: 'admin',
// Numbers
age: 32,
login_count: 150,
score: 95.5,
// Booleans
is_active: true,
trial_user: false,
// Dates (ISO 8601 strings recommended)
signup_date: '2024-01-15',
last_login: '2024-12-26T10:30:00Z',
// Arrays (as comma-separated strings)
permissions: 'admin,billing,analytics',
// Objects (as JSON strings)
preferences: JSON.stringify({
theme: 'dark',
notifications: true
})
}
});
Naming Conventions
Use snake_case for consistency with Pendo's data model:
// Good: snake_case
pendo.initialize({
visitor: {
id: 'user-123',
full_name: 'John Doe',
signup_date: '2024-01-15',
is_trial_user: false
}
});
// Avoid: camelCase or PascalCase
pendo.initialize({
visitor: {
id: 'user-123',
fullName: 'John Doe', // Avoid
signupDate: '2024-01-15', // Avoid
IsTrialUser: false // Avoid
}
});
Updating Visitor and Account Data
Update During Session
Update visitor or account metadata after initialization:
// Update visitor role after permission change
pendo.updateOptions({
visitor: {
role: 'super_admin',
permissions: 'admin,billing,analytics,settings'
}
});
// Update account plan after upgrade
pendo.updateOptions({
account: {
plan: 'enterprise',
mrr: 5000,
seats_purchased: 150
}
});
Incremental Updates
You can update individual fields without resending all data:
// User completes onboarding
pendo.updateOptions({
visitor: {
onboarding_completed: true,
onboarding_completion_date: new Date().toISOString()
}
});
// User logs in
pendo.updateOptions({
visitor: {
last_login: new Date().toISOString(),
login_count: user.loginCount + 1
}
});
Dynamic Data Loading
From API
async function initializePendoFromAPI() {
try {
const response = await fetch('/api/user/pendo-data');
const data = await response.json();
pendo.initialize({
visitor: data.visitor,
account: data.account
});
} catch (error) {
console.error('Failed to initialize Pendo:', error);
}
}
// Call after authentication
initializePendoFromAPI();
From Data Layer
// Wait for data layer to be ready
function waitForDataLayer() {
if (window.dataLayer && window.dataLayer.user) {
const user = window.dataLayer.user;
const account = window.dataLayer.account;
pendo.initialize({
visitor: {
id: user.id,
email: user.email,
full_name: user.name,
role: user.role
},
account: {
id: account.id,
name: account.name,
plan: account.plan
}
});
} else {
setTimeout(waitForDataLayer, 100);
}
}
waitForDataLayer();
From Local Storage
// Initialize from stored user data
const userData = JSON.parse(localStorage.getItem('user'));
const accountData = JSON.parse(localStorage.getItem('account'));
if (userData && accountData) {
pendo.initialize({
visitor: {
id: userData.id,
email: userData.email,
full_name: userData.fullName,
role: userData.role
},
account: {
id: accountData.id,
name: accountData.name,
plan: accountData.plan
}
});
}
Framework-Specific Implementations
React with State
import { useEffect } from 'react';
import { useAuth } from './hooks/useAuth';
function App() {
const { user, account } = useAuth();
useEffect(() => {
if (user && account && window.pendo) {
window.pendo.initialize({
visitor: {
id: user.id,
email: user.email,
full_name: user.fullName,
role: user.role,
signup_date: user.createdAt,
plan_type: account.plan
},
account: {
id: account.id,
name: account.name,
plan: account.plan,
mrr: account.monthlyRecurringRevenue,
total_users: account.userCount
}
});
}
}, [user, account]);
return <YourApp />;
}
Angular with RxJS
import { Component, OnInit } from '@angular/core';
import { combineLatest } from 'rxjs';
import { AuthService } from './services/auth.service';
import { AccountService } from './services/account.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
constructor(
private authService: AuthService,
private accountService: AccountService
) {}
ngOnInit() {
combineLatest([
this.authService.user$,
this.accountService.account$
]).subscribe(([user, account]) => {
if (user && account && window.pendo) {
window.pendo.initialize({
visitor: {
id: user.id,
email: user.email,
full_name: user.fullName,
role: user.role
},
account: {
id: account.id,
name: account.name,
plan: account.plan,
mrr: account.mrr
}
});
}
});
}
}
Vue with Composables
// composables/usePendo.js
import { watch } from 'vue';
import { useAuthStore } from '@/stores/auth';
import { useAccountStore } from '@/stores/account';
export function usePendo() {
const authStore = useAuthStore();
const accountStore = useAccountStore();
watch(
() => [authStore.user, accountStore.account],
([user, account]) => {
if (user && account && window.pendo) {
window.pendo.initialize({
visitor: {
id: user.id,
email: user.email,
full_name: user.fullName,
role: user.role
},
account: {
id: account.id,
name: account.name,
plan: account.plan
}
});
}
},
{ immediate: true }
);
}
Data Privacy and Security
Excluding Sensitive Data
Never include sensitive information in visitor or account metadata:
// BAD: Contains sensitive data
pendo.initialize({
visitor: {
id: 'user-123',
ssn: '123-45-6789', // Never include
credit_card: '4111...', // Never include
password: 'secret123' // Never include
}
});
// GOOD: Only safe metadata
pendo.initialize({
visitor: {
id: 'user-123',
email: 'user@example.com',
role: 'admin',
plan_type: 'enterprise'
}
});
Anonymizing Email Addresses
For privacy compliance, consider hashing emails:
async function hashEmail(email) {
const encoder = new TextEncoder();
const data = encoder.encode(email);
const hash = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
const hashedEmail = await hashEmail(user.email);
pendo.initialize({
visitor: {
id: user.id,
email_hash: hashedEmail, // Use hash instead of plain email
role: user.role
}
});
GDPR Compliance
Respect user consent:
// Only initialize Pendo if user consented
if (userConsentedToAnalytics) {
pendo.initialize({
visitor: {
id: user.id,
email: user.email,
consent_given: true,
consent_date: new Date().toISOString()
}
});
}
Data Validation
Validation Helper
function validatePendoData(visitor, account) {
const errors = [];
// Visitor validation
if (!visitor.id) {
errors.push('Visitor ID is required');
}
if (visitor.email && !isValidEmail(visitor.email)) {
errors.push('Invalid email format');
}
// Account validation
if (account && !account.id) {
errors.push('Account ID is required when account is provided');
}
if (account && account.mrr && typeof account.mrr !== 'number') {
errors.push('MRR must be a number');
}
return errors;
}
// Use validation before initialization
const errors = validatePendoData(visitorData, accountData);
if (errors.length > 0) {
console.error('Pendo data validation failed:', errors);
} else {
pendo.initialize({
visitor: visitorData,
account: accountData
});
}
Common Data Patterns
SaaS Applications
pendo.initialize({
visitor: {
id: user.id,
email: user.email,
full_name: user.fullName,
role: user.role,
signup_date: user.createdAt,
trial_end_date: user.trialEndsAt,
is_paying_customer: user.subscriptionStatus === 'active'
},
account: {
id: account.id,
name: account.name,
plan: account.plan,
mrr: account.monthlyRevenue,
seats_purchased: account.licenses,
seats_used: account.activeUsers,
contract_end_date: account.contractEndsAt
}
});
E-commerce Platforms
pendo.initialize({
visitor: {
id: user.id,
email: user.email,
full_name: user.fullName,
customer_since: user.firstOrderDate,
total_orders: user.orderCount,
total_spent: user.lifetimeValue,
vip_status: user.isVIP,
preferred_category: user.favoriteCategory
},
account: {
id: user.id, // For B2C, account ID often equals visitor ID
customer_tier: user.tier,
loyalty_points: user.points
}
});
Enterprise Applications
pendo.initialize({
visitor: {
id: user.employeeId,
email: user.email,
full_name: user.fullName,
department: user.department,
job_title: user.title,
manager_id: user.managerId,
office_location: user.office,
access_level: user.securityClearance
},
account: {
id: company.id,
name: company.name,
division: company.division,
region: company.region,
employee_count: company.employeeCount
}
});
Debugging Data Layer
Enable Debug Mode
pendo.setDebugMode(true);
// Check visitor data
console.log('Visitor ID:', pendo.getVisitorId());
console.log('Account ID:', pendo.getAccountId());
// Inspect full configuration
console.log('Pendo Config:', window.pendo.getConfig());
Verify Data in Pendo
- Log into Pendo dashboard
- Navigate to Settings > Data Mappings
- Check "Visitor Metadata" and "Account Metadata"
- Verify your custom fields appear
- Confirm data types are correct