Skip to main content

Klaviyo Integration - Implementation Details

Architecture-level overview of the Klaviyo integration. Phase 5 (Mobile SDK) is complete. Phases 1-4 (Backend) remain.


Architecture Overview


Component Breakdown

1. Backend: Klaviyo Module --- NOT STARTED

Location: eli-backend-api/src/modules/klaviyo/

Structure:

klaviyo/
├── klaviyo.module.ts # Module definition
├── klaviyo.service.ts # API calls to Klaviyo
└── klaviyo.types.ts # TypeScript interfaces

Dependencies:

  • ShopifyModule - For email resolution
  • UsersModule - For user data

Responsibilities:

  • Call Klaviyo API to update profiles
  • Resolve correct email (Shopify vs app)
  • Calculate test metrics
  • Handle API errors gracefully

2. Backend: Hook Point --- NOT STARTED

Location: eli-backend-api/src/modules/readings/readings.controller.ts

Method: handleAnalysis()

When: After reading status changes to Completed

Pattern: Same as existing sendNotification() - fire and forget, don't block test completion

3. Backend: Email Resolution --- NOT STARTED

Uses: Existing shopify.service.ts

Method: getCustomerInfo(userId)

Returns: Customer email from Shopify

Flow:

  1. Check if user has shopifyCustomerId
  2. If yes, call Shopify GraphQL API
  3. Return customer.emailAddress.emailAddress
  4. If no, fall back to user.email from database

4. Backend: Test Stock Calculation --- NOT STARTED

Data Sources:

SourceForAPI/Table
LoopSubscribersBigQuery: eli_health_loopwork.subscriptions
ShopifyOne-time buyersshopify.service.getOrders()

Logic:

  • Subscribers: remaining = testsInPlan - (totalCompleted % testsInPlan)
  • One-time: remaining = quantityPurchased - totalCompleted

5. Batch Job: Daily Metrics --- NOT STARTED

Location: eli-kpi (new batch job)

Trigger: Cloud Scheduler, daily at 6 AM

Calculates:

  • days_since_last_test = TODAY - last_test_date
  • avg_tests_per_week = tests in last 30 days / 4.3

Flow:

6. Mobile: Klaviyo SDK --- DONE (EXM-996, Zeeshan, 2026-01-27)

Package: klaviyo-react-native-sdk

Initialization: IIFE in eli-app/src/services/klaviyo.ts (runs at module import time)

Environment: REACT_APP_KLAVIYO_API_KEY via .env-cmdrc.jsenvironment.klaviyoApiKey

Service (src/services/klaviyo.ts):

MethodWhen CalledWhat It Does
initialize()Module load (IIFE)Klaviyo.initialize(apiKey)
setProfile(user)Login (email, Google, Apple) + app re-authSets email, externalId, firstName, lastName, timezone
resetProfile()Sign outClears Klaviyo identity
setPushToken(token)FCM token retrievalForwards Firebase push token to Klaviyo
createEvent(event)Measurement completion, notification openedSends custom events to Klaviyo

Integration points across eli-app:

FileHookKlaviyo Call
services/auth.tscheckIfAuthenticated()App startup with existing sessionsetProfile(user)
services/auth.tssignIn()Email/password loginsetProfile(user)
services/auth.tssignInWithGoogle()Google OAuth loginsetProfile(user)
services/auth.tssignInWithApple()Apple OAuth loginsetProfile(user)
services/auth.tssignOut()User logs outresetProfile()
services/messaging.tsgetFCMToken()FCM token obtainedsetPushToken(fcmToken)
services/app.tsonNotificationOpenedUser opens a protocol reminder notificationcreateEvent('reminder_notification_opened')
screens/.../MeasurementCalculating.tsxReading status = completedcreateEvent('measurement_completed')

Notification data enhancement (enables Klaviyo event tracking):

Local notifications (Notifee) now include userId, hormoneType, and type: 'protocol_reminder' in their data payload. Files changed: notification.ts, protocolNotifications.ts, useHormoneMeasurementState.tsx, ProtocolBuilderScreen.tsx, TestingProtocolsScreen.tsx, RemindersScreen.tsx.

Not yet implemented (Phase 5b):

  • In-app message rendering (banners, modals from Klaviyo campaigns)
  • Klaviyo push notification handling (handlePush routing)
  • app_first_opened event tracking

API Specifications

Klaviyo Profile Import API

Endpoint: POST https://a.klaviyo.com/api/profile-import/

Headers:

Authorization: Klaviyo-API-Key {KLAVIYO_API_KEY}
revision: 2024-10-15
Content-Type: application/json

Body Structure:

{
"data": {
"type": "profile",
"attributes": {
"email": "user@example.com",
"properties": {
"total_tests_completed": 5,
"last_test_date": "2025-01-26T10:30:00Z",
"first_test_completed": true,
"test_stock_percentage": 60,
"tests_remaining": 3,
"subscription_plan": "8_tests"
}
}
}
}

Rate Limits: 75 requests/second

Shopify Customer API

Already implemented in shopify.service.ts

Endpoint: https://account.eli.health/customer/api/2025-10/graphql

Returns: Customer ID, email, first/last name


Environment Variables

Backend (eli-backend-api/.env) --- NOT YET CONFIGURED:

KLAVIYO_API_KEY=pk_xxx          # Private API key
KLAVIYO_API_REVISION=2024-10-15 # API version

Mobile (eli-app) --- DONE:

REACT_APP_KLAVIYO_API_KEY=xxx   # Public API key for SDK init

Configured in .env-cmdrc.js (reads from process.env.REACT_APP_KLAVIYO_API_KEY), typed in src/types/env.d.ts, exposed via environment.klaviyoApiKey in src/config/environment.ts.

Note: The current config uses a single key from process.env for all environments. The Klaviyo Multi-Environment Guide specifies separate keys for dev/staging (test account eli-health-test) and production (eli-health). Per-environment keys should be injected via the CI/CD pipeline.

Multi-environment setup: See Klaviyo Multi-Environment Guide for dev/staging/production configuration.


Error Handling Strategy

Key Principle: Klaviyo failures must NEVER block test completion.


Data Flow Summary

Real-Time (Every Test)

Properties Updated:

  • total_tests_completed
  • last_test_date
  • first_test_completed (if first)
  • days_to_first_test (if first)
  • test_stock_percentage
  • tests_remaining
  • subscription_plan

Daily Batch (6 AM)

Properties Updated:

  • days_since_last_test
  • avg_tests_per_week

On Profile Update

Properties Updated:

  • user_gender
  • user_age
  • purchase_motivation
  • hormone_tracking_goal

Integration Tips

Tip 1: Follow the Notification Pattern

Look at sendNotification() in readings.controller.ts:

  • Fire and forget (don't await)
  • Try-catch to prevent failures from bubbling
  • Log success/failure

Tip 2: Use Existing Shopify Service

Don't reinvent email resolution:

shopifyService.getCustomerInfo(userId)

Returns email, first name, last name.

Tip 3: Module Structure

Copy shopify.module structure:

  • Clean, minimal module definition
  • Service handles all business logic
  • Types in separate file

Tip 4: Idempotent Updates

Klaviyo profile-import is idempotent:

  • Same email = update existing profile
  • No need to check if profile exists first

Tip 5: Batch Job Location

Add to eli-kpi since:

  • Already has BigQuery access
  • Already has Cloud Scheduler setup
  • Pattern exists for other batch jobs

Testing Strategy

Manual Testing

  1. Complete a test in staging app
  2. Check Klaviyo staging profile updated
  3. Verify all properties correct
  4. Test with Shopify-linked and unlinked users

Validation Queries

-- Check users with test data
SELECT email, total_tests, last_test_date
FROM `eli-health-prod.eli_health_biometricspublic.user` u
JOIN `eli-health-prod.eli_health_biometricspublic.reading` r
ON u.id = r.userId
WHERE r.status = 'completed'
LIMIT 10;

Monitoring

  • Log all Klaviyo API calls
  • Alert on auth errors (401)
  • Track success rate (target: 99%+)


Document History

DateAuthorChanges
2026-01-27ChipUpdated with Phase 5 implementation (EXM-996, Zeeshan). Marked all component statuses. Updated architecture diagram and env vars.
2025-01-26ChipInitial implementation details