Skip to main content

LoopWork Data Integration

Overview

Loop Subscriptions (LoopWork) is Eli Health's subscription management platform that runs on top of Shopify. This document describes how we synchronize subscription data from Loop to BigQuery for analytics and reporting.

Why LoopWork Data Sync?

Unlike Shopify (which uses Airbyte), Loop Subscriptions requires a custom synchronization solution because:

  • Airbyte doesn't have a native Loop Subscriptions connector
  • Loop provides rich subscription lifecycle data not available in Shopify
  • We need incremental sync to minimize API calls and respect rate limits
  • Subscription analytics require joining Loop data with Shopify and Firebase data

What Data is Synced?

ResourceDescriptionData Points
SubscriptionsAll subscription contractsStatus, billing/delivery policies, line items, discounts, customer references
CustomersSubscription customersContact info, subscription counts by status
OrdersSubscription-related ordersOrder details, pricing, fulfillment status
ProductsSubscription productsProduct variants, pricing, SKUs

Architecture

API Configuration

Base URL & Authentication

SettingValue
Base URLhttps://api.loopsubscriptions.com/admin/2023-10/
Authentication Headerx-loop-token
Token SourceLoop Admin Portal → Settings → Manage API tokens

Available Endpoints

EndpointHTTP MethodRate LimitDescription
/subscriptionGET2 req/3sList all subscriptions
/customerGET1 req/sList all customers
/orderGET2 req/3sList all orders
/productGET6 req/sList products with variants

Rate Limits

Loop uses fixed-window rate limiting with endpoint-specific pools:

  • Global Pool: 10 requests per second (applies to endpoints without specific limits)
  • Endpoint-Specific Limits: See table above (isolated from global pool)

Best Practices:

  • Implement separate rate limiters for each endpoint
  • Add jitter to requests to avoid synchronized bursts
  • Handle HTTP 429 responses with exponential backoff

Pagination

All list endpoints use page-based pagination:

{
"success": true,
"message": "Subscriptions fetched successfully",
"data": [...],
"code": "SUCCESS",
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false
}
}
ParameterTypeDescription
pageNoint32Page number (starts from 1)
pageSizeint32Items per page (max 50)

BigQuery Dataset

Dataset Configuration

SettingValue
Projecteli-health-prod
Dataset IDeli_health_loopwork
Locationnorthamerica-northeast1 (Montreal)

Important: The dataset location matches eli_health_shopify to enable cross-dataset joins.

Table Schemas

subscriptions

ColumnTypeDescription
idINT64Loop subscription ID
shopify_idINT64Shopify subscription contract ID
origin_order_shopify_idINT64Original order ID
statusSTRINGACTIVE, PAUSED, CANCELLED, EXPIRED
created_atTIMESTAMPSubscription creation time
updated_atTIMESTAMPLast update time
cancelled_atTIMESTAMPCancellation time (if cancelled)
cancellation_reasonSTRINGReason for cancellation
currency_codeSTRINGCurrency (CAD, USD, etc.)
total_line_item_priceFLOAT64Total price before discounts
delivery_priceFLOAT64Shipping cost
completed_orders_countINT64Number of fulfilled orders
is_prepaidBOOLWhether subscription is prepaid
last_payment_statusSTRINGSUCCESS, FAILED, etc.
next_billing_date_epochINT64Next billing timestamp
billing_policyJSONInterval, count, anchor settings
delivery_policyJSONDelivery interval settings
shipping_addressJSONCustomer shipping address
linesJSONLine items with products
discountsJSONApplied discounts
customer_idINT64Loop customer ID
customer_shopify_idINT64Shopify customer ID
_synced_atTIMESTAMPSync timestamp

customers

ColumnTypeDescription
idINT64Loop customer ID
shopify_idINT64Shopify customer ID
first_nameSTRINGFirst name
last_nameSTRINGLast name
emailSTRINGEmail address
phoneSTRINGPhone number
active_subscriptions_countINT64Active subscription count
paused_subscriptions_countINT64Paused subscription count
cancelled_subscriptions_countINT64Cancelled subscription count
all_subscriptions_countINT64Total subscription count
_synced_atTIMESTAMPSync timestamp

orders

ColumnTypeDescription
idINT64Loop order ID
shopify_idINT64Shopify order ID
shopify_order_numberINT64Human-readable order number
statusSTRINGPROCESSED, PENDING, etc.
financial_statusSTRINGpaid, refunded, etc.
fulfillment_statusSTRINGFulfillment state
billing_date_epochINT64Billing timestamp
currency_codeSTRINGCurrency code
total_priceFLOAT64Total order price
total_price_usdFLOAT64Price in USD
total_discountFLOAT64Total discounts
total_shipping_priceFLOAT64Shipping cost
total_taxFLOAT64Tax amount
shopify_created_atTIMESTAMPShopify creation time
shopify_updated_atTIMESTAMPShopify update time
shipping_addressJSONShipping address
discount_codesJSONApplied discount codes
linesJSONOrder line items
subscription_idINT64Related subscription ID
customer_idINT64Loop customer ID
_synced_atTIMESTAMPSync timestamp

products

ColumnTypeDescription
shopify_idINT64Shopify product ID
titleSTRINGProduct title
variantsJSONProduct variants with SKUs, prices
_synced_atTIMESTAMPSync timestamp

Sync Implementation

Sync Strategy: Incremental with Full Refresh

The sync uses an incremental strategy with periodic full refresh:

  1. Hourly Incremental Sync: Fetch records updated since last sync using updated_at timestamp
  2. Daily Full Refresh: Complete sync of all records to catch any missed updates
  3. Upsert Logic: Records are upserted based on primary key (id)

Sync Endpoint in eli-kpi

The sync job is implemented as a Cloud Run endpoint in the eli-kpi service:

POST /sync/loopwork

Trigger: Cloud Scheduler job running every hour

Implementation Steps

Phase 1: BigQuery Dataset Setup

-- Create dataset in Montreal region (same as Shopify data)
CREATE SCHEMA IF NOT EXISTS `eli-health-prod.eli_health_loopwork`
OPTIONS (
location = 'northamerica-northeast1',
description = 'Loop Subscriptions data synchronized from LoopWork API'
);

Phase 2: Secret Management

  1. Add token to Google Secret Manager:
gcloud secrets create loopwork-api-token \
--project=eli-health-prod \
--replication-policy="automatic"

echo -n "YOUR_LOOPWORK_TOKEN" | gcloud secrets versions add loopwork-api-token --data-file=-
  1. Grant access to KPI service account:
gcloud secrets add-iam-policy-binding loopwork-api-token \
--project=eli-health-prod \
--member="serviceAccount:kpi-service-us@eli-health-prod.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
  1. Add to GitHub Secrets for deploy.yml:
    • Add LOOPWORK_API_TOKEN to eli-kpi repository secrets
    • Update deploy.yml to include: --set-env-vars="LOOPWORK_API_TOKEN=${{ secrets.LOOPWORK_API_TOKEN }}"

Phase 3: Sync Service Implementation

Create /src/loopwork-sync.ts in eli-kpi with:

  • API client with rate limiting
  • Pagination handling
  • BigQuery upsert logic
  • Error handling and retries

Phase 4: Cloud Scheduler Setup

Note: The Cloud Scheduler job is now managed by Terraform in eli-devops/tf/modules/regional/kpi-compute/main.tf. Do not create it manually with gcloud.

Terraform Resource:

# eli-devops/tf/modules/regional/kpi-compute/main.tf
resource "google_cloud_scheduler_job" "loopwork_sync" {
name = "loopwork-hourly-sync"
description = "Hourly sync of Loop Subscriptions data to BigQuery"
schedule = "0 * * * *" # Every hour at minute 0
time_zone = "America/Toronto"
attempt_deadline = "600s"
region = var.gcp.location # us-east1

retry_config {
retry_count = 3
min_backoff_duration = "30s"
max_backoff_duration = "300s"
max_doublings = 3
}

http_target {
http_method = "POST"
uri = "${google_cloud_run_v2_service.kpi.uri}/sync/loopwork"

oidc_token {
service_account_email = google_service_account.kpi.email
audience = google_cloud_run_v2_service.kpi.uri
}
}
}

To apply: Run terraform apply in eli-devops/tf with the appropriate environment.

Legacy gcloud command (for reference only)
gcloud scheduler jobs create http loopwork-hourly-sync \
--project=eli-health-prod \
--location=us-east1 \
--schedule="0 * * * *" \
--uri="https://kpi.app.eli.health/sync/loopwork" \
--http-method=POST \
--oidc-service-account-email=kpi-service-us@eli-health-prod.iam.gserviceaccount.com \
--oidc-token-audience="https://kpi.app.eli.health"

Current Data Volume

As of November 2025:

ResourceRecord CountEstimated Sync Time
Subscriptions~2,200~70 seconds
Customers~2,000+~40 seconds
Orders~1,500+~50 seconds
Products~20~1 second

Total estimated sync time: ~3 minutes for full refresh

Monitoring & Maintenance

Monitoring

  1. Cloud Run Logs: Check sync job logs for errors
  2. Cloud Scheduler Console: Verify job execution history
  3. BigQuery: Monitor _synced_at timestamps for freshness

Key Metrics to Track

  • Sync job success rate
  • Sync duration
  • Record count changes
  • API error rates

Common Issues

Rate Limit Errors (HTTP 429)

Symptoms: Sync fails with 429 status codes

Resolution:

  1. Check rate limiter implementation
  2. Add more delay between requests
  3. Implement exponential backoff

Stale Data

Symptoms: BigQuery data older than expected

Resolution:

  1. Check Cloud Scheduler job status
  2. Verify Cloud Run service is healthy
  3. Check API token validity

Schema Drift

Symptoms: New fields in API response not in BigQuery

Resolution:

  1. Update BigQuery table schema
  2. Modify sync code to handle new fields
  3. Re-run full sync

Querying LoopWork Data

Example: Subscription Status Distribution

SELECT
status,
COUNT(*) as count,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER(), 2) as percentage
FROM `eli-health-prod.eli_health_loopwork.subscriptions`
GROUP BY status
ORDER BY count DESC;

Example: Join with Shopify Orders

-- Link Loop subscriptions to Shopify order details
SELECT
l.id as loop_subscription_id,
l.status as subscription_status,
s.id as shopify_order_id,
s.total_price as shopify_total,
s.created_at as order_date
FROM `eli-health-prod.eli_health_loopwork.subscriptions` l
JOIN `eli-health-prod.eli_health_shopify.orders` s
ON l.origin_order_shopify_id = s.id
WHERE l.status = 'ACTIVE'
ORDER BY s.created_at DESC
LIMIT 100;

Example: Customer Subscription Analytics

-- Active customers with their subscription value
SELECT
c.email,
c.first_name,
c.last_name,
c.active_subscriptions_count,
SUM(s.total_line_item_price) as total_subscription_value
FROM `eli-health-prod.eli_health_loopwork.customers` c
JOIN `eli-health-prod.eli_health_loopwork.subscriptions` s
ON c.id = s.customer_id
WHERE c.active_subscriptions_count > 0
GROUP BY 1, 2, 3, 4
ORDER BY total_subscription_value DESC
LIMIT 50;

Security Considerations

Token Management

  • Never commit tokens: Store in Secret Manager or GitHub Secrets
  • Rotate regularly: Update token quarterly
  • Minimum scope: Only request necessary API permissions

Data Protection

  • Encryption at rest: BigQuery encrypts all data by default
  • Access control: Use BigQuery IAM to restrict access
  • PII handling: Customer data should be accessed only by authorized personnel

Support & Contact

For issues with LoopWork integration:

  1. Check Logs: Cloud Run logs in GCP Console
  2. API Issues: Contact Loop support at support@loopwork.co
  3. Internal Contact: DevOps team at Eli Health

Last Updated: November 2025 Maintained By: Chip (chip@eli.health) API Version: 2023-10