Introduction
Onboarding is the first impression that determines customer success. 60% of customers cite onboarding quality as a key factor in retention. This guide covers building automated, scalable onboarding systems.
Key Statistics:
- 63% of customers decide to continue or churn within 60 seconds
- Automated onboarding reduces time-to-value by 70%
- Companies with great onboarding have 2x higher LTV
- 40% of churn happens within first 90 days
What Is SaaS Onboarding and Why It Matters
SaaS onboarding is the process of getting new customers up and running with your product. It’s not just about creating an accountโit’s about helping users understand your product, configure it for their needs, and achieve their first “aha moment.”
The Onboarding Funnel
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SaaS Onboarding Funnel โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ 1. Trial Sign-up (100%) โ
โ โโโ User creates account โ
โ โ
โ 2. Account Setup (80%) โ
โ โโโ User completes initial configuration โ
โ โ
โ 3. First Value Moment (60%) โ
โ โโโ User achieves first meaningful outcome โ
โ โ
โ 4. Active Usage (40%) โ
โ โโโ User regularly uses the product โ
โ โ
โ 5. Paid Conversion (25%) โ
โ โโโ User upgrades to paid plan โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Common Onboarding Challenges
1. User Confusion
- Users don’t understand how to use your product
- Too many options overwhelm new users
- Documentation is hard to find or understand
2. Configuration Complexity
- Users struggle to configure the product for their needs
- Integration setup is complicated
- Data migration is error-prone
3. Time-to-Value
- Users don’t see value quickly enough
- Onboarding takes too long
- Features are buried deep in the UI
4. Integration Gaps
- Users need to connect to other systems
- API documentation is unclear
- Webhook setup is complex
User Provisioning
Understanding User Provisioning
User provisioning is the automated process of creating, managing, and deactivating user accounts across systems. In SaaS, provisioning ensures that when a user joins an organization, they get the right access to your product, and when they leave, their access is properly revoked.
SCIM vs Manual Provisioning
SCIM (System for Cross-domain Identity Management) is an open standard for automating user provisioning. It defines a RESTful API that identity providers (IdPs) can use to create, update, and delete user accounts.
Manual Provisioning involves administrators manually creating accounts through your UI or API. This is error-prone and doesn’t scale.
Why SCIM Matters
- Automated Onboarding: New employees get immediate access when added to the IdP
- Automated Offboarding: Access is revoked immediately when employees leave
- Consistent User Data: User attributes are synchronized from the IdP
- Reduced Admin Work: No manual account creation or deletion
- Compliance: Audit trails for user provisioning activities
SCIM 2.0 Core Schema
SCIM defines a standard schema for user objects:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "2819c223-7f76-453a-919d-413861904646",
"externalId": "jsmith",
"userName": "[email protected]",
"name": {
"formatted": "John Smith",
"familyName": "Smith",
"givenName": "John"
},
"emails": [
{
"value": "[email protected]",
"type": "work",
"primary": true
}
],
"active": true,
"groups": [
{
"value": "e9e30dba-f08f-4109-a794-5445929a4568",
"$ref": "https://example.com/scim/v2/Groups/e9e30dba-f08f-4109-a794-5445929a4568",
"display": "Sales"
}
],
"meta": {
"resourceType": "User",
"created": "2010-01-23T00:00:00Z",
"lastModified": "2010-01-23T00:00:00Z",
"location": "https://example.com/scim/v2/Users/2819c223-7f76-453a-919d-413861904646",
"version": "W/\"e180ee84f0671b1\""
}
}
SCIM Endpoints
SCIM defines several endpoints for user management:
| Endpoint | Method | Description |
|---|---|---|
/scim/v2/Users |
GET | List users with pagination |
/scim/v2/Users |
POST | Create a new user |
/scim/v2/Users/{id} |
GET | Get a specific user |
/scim/v2/Users/{id} |
PUT | Update a user (full replacement) |
/scim/v2/Users/{id} |
PATCH | Update a user (partial update) |
/scim/v2/Users/{id} |
DELETE | Delete a user |
/scim/v2/Groups |
GET | List groups |
/scim/v2/Groups |
POST | Create a new group |
SCIM Implementation
#!/usr/bin/env python3
"""SCIM 2.0 server implementation."""
from flask import Flask, request, jsonify
import uuid
app = Flask(__name__)
# In-memory storage
users = {}
groups = {}
@app.route('/scim/v2/Users', methods=['GET'])
def list_users():
"""List users with pagination."""
start_index = int(request.args.get('startIndex', 1))
count = int(request.args.get('count', 100))
user_list = list(users.values())[start_index-1:start_index-1+count]
return jsonify({
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": len(users),
"startIndex": start_index,
"itemsPerPage": count,
"Resources": [format_user(u) for u in user_list]
})
@app.route('/scim/v2/Users', methods=['POST'])
def create_user():
"""Create new user."""
data = request.json
user_id = str(uuid.uuid4())
user = {
"id": user_id,
"userName": data.get('userName'),
"name": data.get('name', {}),
"emails": data.get('emails', []),
"groups": data.get('groups', []),
"active": data.get('active', True),
"meta": {
"resourceType": "User",
"created": datetime.now().isoformat()
}
}
users[user_id] = user
# Provision resources for new user
provision_user_resources(user)
return jsonify(format_user(user)), 201
@app.route('/scim/v2/Users/<user_id>', methods=['PUT'])
def update_user(user_id):
"""Update user."""
if user_id not in users:
return jsonify({"error": "User not found"}), 404
data = request.json
users[user_id].update(data)
return jsonify(format_user(users[user_id]))
@app.route('/scim/v2/Users/<user_id>', methods=['DELETE'])
def delete_user(user_id):
"""Delete user and deprovision."""
if user_id not in users:
return jsonify({"error": "User not found"}), 404
# Deprovision user resources
deprovision_user_resources(users[user_id])
del users[user_id]
return '', 204
def format_user(user):
"""Format user for SCIM response."""
return {
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": user["id"],
"userName": user["userName"],
"name": user.get("name", {}),
"emails": user.get("emails", []),
"active": user.get("active", True),
"meta": user.get("meta", {})
}
def provision_user_resources(user):
"""Provision resources for new user."""
# Create default workspace
create_workspace(user)
# Add to default team
add_user_to_default_team(user)
# Generate API keys
create_api_keys(user)
# Trigger welcome email
send_welcome_email(user)
def deprovision_user_resources(user):
"""Remove user access and resources."""
# Revoke API keys
revoke_api_keys(user)
# Archive user data
archive_user_data(user)
JIT Provisioning
#!/usr/bin/env python3
"""Just-In-Time user provisioning."""
import jwt
from datetime import datetime, timedelta
class JITProvisioner:
def __init__(self, config):
self.secret_key = config['jwt_secret']
self.idp = config['identity_provider']
def process_saml_assertion(self, assertion):
"""Process SAML assertion and provision user."""
# Extract user attributes
user_attrs = {
'email': assertion.get('email'),
'name': assertion.get('name'),
'groups': assertion.get('groups', []),
'department': assertion.get('department'),
'manager': assertion.get('manager')
}
# Find or create user
user = self.find_user_by_email(user_attrs['email'])
if not user:
user = self.create_user(user_attrs)
else:
self.update_user(user, user_attrs)
# Assign to groups based on IdP groups
self.sync_groups(user, user_attrs['groups'])
# Grant role based on department
self.assign_role(user, user_attrs.get('department'))
return user
def process_jwt_token(self, token):
"""Process JWT token for JWT-based provisioning."""
try:
payload = jwt.decode(token, self.secret_key, algorithms=['HS256'])
user_attrs = {
'email': payload['email'],
'name': payload.get('name'),
'roles': payload.get('roles', []),
'tenant_id': payload.get('tenant_id')
}
return self.provision_with_tenant(user_attrs, payload['tenant_id'])
except jwt.InvalidTokenError as e:
raise Unauthorized(str(e))
Provisioning Workflow
#!/usr/bin/env python3
"""Complete provisioning workflow."""
from datetime import datetime
class ProvisioningWorkflow:
"""Orchestrate user provisioning workflow."""
def __init__(self, db, email_service, notification_service):
self.db = db
self.email = email_service
self.notifications = notification_service
def provision_user(self, user_data):
"""Complete user provisioning workflow."""
# Step 1: Create user account
user = self.create_user_account(user_data)
# Step 2: Provision resources
self.provision_resources(user)
# Step 3: Send welcome email
self.send_welcome_email(user)
# Step 4: Create onboarding tasks
self.create_onboarding_tasks(user)
# Step 5: Notify team
self.notify_team(user)
return user
def create_user_account(self, user_data):
"""Create user account in system."""
# Create user record
user = {
'id': str(uuid.uuid4()),
'email': user_data['email'],
'name': user_data['name'],
'tenant_id': user_data['tenant_id'],
'role': user_data.get('role', 'user'),
'status': 'pending',
'created_at': datetime.utcnow(),
'provisioned_at': None
}
self.db.users.insert(user)
return user
def provision_resources(self, user):
"""Provision resources for user."""
# Create user workspace
workspace = self.db.workspaces.insert({
'user_id': user['id'],
'name': f"{user['name']}'s Workspace",
'created_at': datetime.utcnow()
})
# Create default team
team = self.db.teams.find_one({
'tenant_id': user['tenant_id'],
'name': 'Default Team'
})
if not team:
team = self.db.teams.insert({
'tenant_id': user['tenant_id'],
'name': 'Default Team',
'created_at': datetime.utcnow()
})
# Add user to team
self.db.team_members.insert({
'team_id': team['id'],
'user_id': user['id'],
'role': 'member',
'joined_at': datetime.utcnow()
})
# Generate API keys
api_key = self.generate_api_key(user['id'])
self.db.api_keys.insert({
'user_id': user['id'],
'key': api_key,
'created_at': datetime.utcnow()
})
def send_welcome_email(self, user):
"""Send welcome email to new user."""
template_data = {
'user_name': user['name'],
'product_name': 'Your SaaS Product',
'onboarding_link': f"https://app.example.com/onboarding?user_id={user['id']}"
}
self.email.send(
to=user['email'],
subject='Welcome to Your SaaS Product!',
template='welcome',
template_data=template_data
)
def create_onboarding_tasks(self, user):
"""Create onboarding tasks for user."""
tasks = [
{
'user_id': user['id'],
'title': 'Complete Your Profile',
'description': 'Add your company information and team members',
'priority': 'high',
'created_at': datetime.utcnow()
},
{
'user_id': user['id'],
'title': 'Connect Your First Integration',
'description': 'Connect your favorite tools and services',
'priority': 'medium',
'created_at': datetime.utcnow()
},
{
'user_id': user['id'],
'title': 'Invite Your Team',
'description': 'Collaborate with your team members',
'priority': 'low',
'created_at': datetime.utcnow()
}
]
self.db.tasks.insert_many(tasks)
def notify_team(self, user):
"""Notify team about new user."""
team_members = self.db.team_members.find({
'team_id': user['team_id'],
'role': 'admin'
})
for admin in team_members:
self.notifications.send(
user_id=admin['user_id'],
type='new_user_joined',
message=f"{user['name']} has joined your team",
link=f"/users/{user['id']}"
)
def deprovision_user(self, user_id):
"""Complete user deprovisioning workflow."""
# Step 1: Revoke access
self.revoke_access(user_id)
# Step 2: Archive data
self.archive_data(user_id)
# Step 3: Delete resources
self.delete_resources(user_id)
# Step 4: Update user status
self.update_user_status(user_id, 'deactivated')
def revoke_access(self, user_id):
"""Revoke all user access."""
# Revoke API keys
self.db.api_keys.update_many(
{'user_id': user_id},
{'$set': {'revoked': True, 'revoked_at': datetime.utcnow()}}
)
# Revoke sessions
self.db.sessions.delete_many({'user_id': user_id})
# Revoke OAuth tokens
self.db.oauth_tokens.delete_many({'user_id': user_id})
def archive_data(self, user_id):
"""Archive user data for compliance."""
# Export user data
user_data = self.db.users.find_one({'id': user_id})
activities = self.db.activities.find({'user_id': user_id})
archive = {
'user_id': user_id,
'data': {
'user': user_data,
'activities': list(activities)
},
'archived_at': datetime.utcnow()
}
self.db.archives.insert(archive)
def delete_resources(self, user_id):
"""Delete user resources."""
# Delete user workspace
self.db.workspaces.delete_many({'user_id': user_id})
# Delete user tasks
self.db.tasks.delete_many({'user_id': user_id})
# Delete user notifications
self.db.notifications.delete_many({'user_id': user_id})
API Integration
Understanding API Integration
API integration is the process of connecting your SaaS product with other systems and services. This enables data flow between systems, automates workflows, and extends functionality.
Why API Integration Matters
- Data Synchronization: Keep data consistent across systems
- Workflow Automation: Automate repetitive tasks
- Extended Functionality: Add features through integrations
- Better User Experience: Seamless experience across tools
- Competitive Advantage: Integration capabilities differentiate your product
Common Integration Patterns
1. REST API Integration
- Standard HTTP methods (GET, POST, PUT, DELETE)
- JSON or XML data format
- Stateful or stateless communication
2. Webhook Integration
- Event-driven architecture
- Push notifications to external systems
- Real-time data synchronization
3. OAuth Integration
- Secure authorization without sharing credentials
- Token-based access
- Granular permission scopes
4. SCIM Integration
- Automated user provisioning
- Identity synchronization
- Compliance and audit trails
API Design Best Practices
1. Version Your APIs
GET /api/v1/users
GET /api/v2/users
2. Use Consistent Naming
GET /api/v1/users # List users
POST /api/v1/users # Create user
GET /api/v1/users/{id} # Get user
PUT /api/v1/users/{id} # Update user
DELETE /api/v1/users/{id} # Delete user
3. Implement Pagination
GET /api/v1/users?page=1&limit=20
4. Use Proper Status Codes
- 200 OK: Success
- 201 Created: Resource created
- 400 Bad Request: Invalid request
- 401 Unauthorized: Authentication required
- 403 Forbidden: Insufficient permissions
- 404 Not Found: Resource not found
- 429 Too Many Requests: Rate limit exceeded
- 500 Internal Server Error: Server error
5. Rate Limiting
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1612345678
Webhook System
#!/usr/bin/env python3
"""Webhook delivery system."""
import hmac
import hashlib
import requests
import time
from datetime import datetime
import json
class WebhookManager:
def __init__(self, config):
self.webhooks = {}
self.retry_policy = config.get('retry_policy', {
'max_retries': 3,
'backoff_factor': 2,
'timeout': 30
})
def register_webhook(self, event_type, url, secret, filters=None):
"""Register webhook endpoint."""
webhook_id = generate_webhook_id()
self.webhooks[webhook_id] = {
'id': webhook_id,
'event_type': event_type,
'url': url,
'secret': secret,
'filters': filters or {},
'active': True,
'created_at': datetime.now().isoformat()
}
return webhook_id
def deliver_event(self, event_type, payload):
"""Deliver event to registered webhooks."""
# Find matching webhooks
matching = [
w for w in self.webhooks.values()
if w['event_type'] == event_type and w['active']
]
for webhook in matching:
# Apply filters
if not self.matches_filters(payload, webhook['filters']):
continue
# Sign payload
signature = self.sign_payload(payload, webhook['secret'])
# Deliver with retries
self.deliver_with_retry(webhook, payload, signature)
def sign_payload(self, payload, secret):
"""Generate HMAC signature."""
payload_str = json.dumps(payload, sort_keys=True)
signature = hmac.new(
secret.encode(),
payload_str.encode(),
hashlib.sha256
).hexdigest()
return f"sha256={signature}"
def deliver_with_retry(self, webhook, payload, signature):
"""Deliver webhook with retry logic."""
headers = {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Event': webhook['event_type'],
'X-Webhook-ID': webhook['id']
}
for attempt in range(self.retry_policy['max_retries']):
try:
response = requests.post(
webhook['url'],
json=payload,
headers=headers,
timeout=self.retry_policy['timeout']
)
if response.status_code < 400:
self.log_delivery(webhook, payload, 'success')
return
except requests.RequestException as e:
self.log_delivery(webhook, payload, 'error', str(e))
# Exponential backoff
if attempt < self.retry_policy['max_retries'] - 1:
time.sleep(
self.retry_policy['backoff_factor'] ** attempt
)
self.mark_webhook_failed(webhook)
Integration API Design
# OpenAPI specification for SaaS integration
openapi: 3.0.0
info:
title: SaaS Integration API
version: 1.0.0
paths:
/api/v1/customers:
post:
summary: Create customer
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
email:
type: string
plan:
type: string
enum: [free, pro, enterprise]
responses:
201:
description: Customer created
/api/v1/customers/{id}/subscription:
post:
summary: Update subscription
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
plan:
type: string
addons:
type: array
items:
type: string
/api/v1/webhooks:
get:
summary: List webhooks
post:
summary: Create webhook
delete:
summary: Delete webhook
Integration SDK Examples
#!/usr/bin/env python3
"""SaaS Integration SDK."""
import requests
from typing import Dict, List, Optional
class SaaSIntegrationSDK:
"""SDK for SaaS integration."""
def __init__(self, api_key: str, base_url: str = "https://api.example.com"):
self.api_key = api_key
self.base_url = base_url
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
def create_customer(self, customer_data: Dict) -> Dict:
"""Create a new customer."""
response = requests.post(
f"{self.base_url}/api/v1/customers",
json=customer_data,
headers=self.headers
)
response.raise_for_status()
return response.json()
def get_customer(self, customer_id: str) -> Dict:
"""Get customer details."""
response = requests.get(
f"{self.base_url}/api/v1/customers/{customer_id}",
headers=self.headers
)
response.raise_for_status()
return response.json()
def update_customer(self, customer_id: str, customer_data: Dict) -> Dict:
"""Update customer information."""
response = requests.put(
f"{self.base_url}/api/v1/customers/{customer_id}",
json=customer_data,
headers=self.headers
)
response.raise_for_status()
return response.json()
def delete_customer(self, customer_id: str) -> bool:
"""Delete a customer."""
response = requests.delete(
f"{self.base_url}/api/v1/customers/{customer_id}",
headers=self.headers
)
response.raise_for_status()
return response.status_code == 204
def list_customers(self, page: int = 1, limit: int = 20) -> Dict:
"""List customers with pagination."""
response = requests.get(
f"{self.base_url}/api/v1/customers",
params={"page": page, "limit": limit},
headers=self.headers
)
response.raise_for_status()
return response.json()
def create_webhook(self, webhook_data: Dict) -> Dict:
"""Create a new webhook."""
response = requests.post(
f"{self.base_url}/api/v1/webhooks",
json=webhook_data,
headers=self.headers
)
response.raise_for_status()
return response.json()
def list_webhooks(self) -> List[Dict]:
"""List all webhooks."""
response = requests.get(
f"{self.base_url}/api/v1/webhooks",
headers=self.headers
)
response.raise_for_status()
return response.json()
def delete_webhook(self, webhook_id: str) -> bool:
"""Delete a webhook."""
response = requests.delete(
f"{self.base_url}/api/v1/webhooks/{webhook_id}",
headers=self.headers
)
response.raise_for_status()
return response.status_code == 204
Integration Testing
#!/usr/bin/env python3
"""Integration testing for SaaS API."""
import pytest
from unittest.mock import Mock, patch
class TestSaaSIntegration:
"""Test SaaS integration."""
def setup_method(self):
"""Set up test fixtures."""
self.sdk = SaaSIntegrationSDK("test-api-key")
@patch('requests.post')
def test_create_customer(self, mock_post):
"""Test customer creation."""
# Mock response
mock_response = Mock()
mock_response.status_code = 201
mock_response.json.return_value = {
"id": "cust_123",
"name": "Test Customer",
"email": "[email protected]"
}
mock_post.return_value = mock_response
# Test
customer_data = {
"name": "Test Customer",
"email": "[email protected]",
"plan": "pro"
}
result = self.sdk.create_customer(customer_data)
assert result["id"] == "cust_123"
assert result["name"] == "Test Customer"
assert result["email"] == "[email protected]"
@patch('requests.get')
def test_get_customer(self, mock_get):
"""Test customer retrieval."""
# Mock response
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"id": "cust_123",
"name": "Test Customer",
"email": "[email protected]"
}
mock_get.return_value = mock_response
# Test
result = self.sdk.get_customer("cust_123")
assert result["id"] == "cust_123"
assert result["name"] == "Test Customer"
@patch('requests.put')
def test_update_customer(self, mock_put):
"""Test customer update."""
# Mock response
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"id": "cust_123",
"name": "Updated Customer",
"email": "[email protected]"
}
mock_put.return_value = mock_response
# Test
result = self.sdk.update_customer("cust_123", {
"name": "Updated Customer",
"email": "[email protected]"
})
assert result["name"] == "Updated Customer"
Onboarding Workflow
Automated Welcome Series
#!/usr/bin/env python3
"""Automated onboarding workflow."""
from datetime import datetime, timedelta
class OnboardingWorkflow:
def __init__(self):
self.steps = []
def add_step(self, step_type, config):
"""Add onboarding step."""
step = {
'type': step_type,
'config': config,
'order': len(self.steps)
}
self.steps.append(step)
return self
def execute(self, user):
"""Execute onboarding for user."""
for step in self.steps:
self.execute_step(user, step)
def execute_step(self, user, step):
"""Execute individual step."""
if step['type'] == 'email':
send_email(user, step['config'])
elif step['type'] == 'task':
create_task(user, step['config'])
elif step['type'] == 'api_call':
call_integration_api(user, step['config'])
elif step['type'] == 'delay':
schedule_next_step(user, step['config'])
# Build onboarding workflow
workflow = OnboardingWorkflow()
workflow.add_step('email', {
'template': 'welcome',
'delay': 0
})
workflow.add_step('task', {
'title': 'Complete your profile',
'description': 'Add your company information',
'delay': timedelta(hours=1)
})
workflow.add_step('api_call', {
'endpoint': '/api/v1/setup',
'action': 'create_workspace',
'delay': timedelta(hours=2)
})
workflow.add_step('email', {
'template': 'getting_started_guide',
'delay': timedelta(days=1)
})
workflow.add_step('task', {
'title': 'Invite your team',
'description': 'Collaborate with your team',
'delay': timedelta(days=2)
})
Onboarding Metrics
#!/usr/bin/env python3
"""Onboarding metrics tracking."""
from datetime import datetime, timedelta
class OnboardingMetrics:
"""Track onboarding metrics."""
def __init__(self, db):
self.db = db
def calculate_completion_rate(self, start_date: datetime, end_date: datetime) -> float:
"""Calculate onboarding completion rate."""
total_users = self.db.users.count_documents({
'created_at': {'$gte': start_date, '$lte': end_date}
})
completed_users = self.db.users.count_documents({
'created_at': {'$gte': start_date, '$lte': end_date},
'onboarding_complete': True
})
return (completed_users / total_users * 100) if total_users > 0 else 0
def calculate_time_to_value(self, start_date: datetime, end_date: datetime) -> float:
"""Calculate average time to first value."""
pipeline = [
{'$match': {
'created_at': {'$gte': start_date, '$lte': end_date},
'first_value_at': {'$ne': None}
}},
{'$project': {
'time_to_value': {
'$subtract': ['$first_value_at', '$created_at']
}
}},
{'$group': {
'_id': None,
'avg_time_to_value': {'$avg': '$time_to_value'}
}}
]
result = list(self.db.users.aggregate(pipeline))
if result:
return result[0]['avg_time_to_value'] / 1000 / 60 / 60 # Convert to hours
return 0
def calculate_drop_off_points(self, start_date: datetime, end_date: datetime) -> list:
"""Identify where users drop off in onboarding."""
pipeline = [
{'$match': {
'created_at': {'$gte': start_date, '$lte': end_date}
}},
{'$project': {
'completed_steps': {'$size': {'$ifNull': ['$completed_steps', []]}},
'total_steps': 5
}},
{'$group': {
'_id': '$completed_steps',
'count': {'$sum': 1}
}},
{'$sort': {'_id': 1}}
]
return list(self.db.users.aggregate(pipeline))
def get_nps_score(self, start_date: datetime, end_date: datetime) -> float:
"""Calculate Net Promoter Score."""
nps_responses = self.db.nps_responses.count_documents({
'created_at': {'$gte': start_date, '$lte': end_date}
})
promoters = self.db.nps_responses.count_documents({
'created_at': {'$gte': start_date, '$lte': end_date},
'score': {'$gte': 9}
})
detractors = self.db.nps_responses.count_documents({
'created_at': {'$gte': start_date, '$lte': end_date},
'score': {'$lte': 6}
})
return ((promoters - detractors) / nps_responses * 100) if nps_responses > 0 else 0
Onboarding Checklist
#!/usr/bin/env python3
"""Onboarding checklist system."""
from datetime import datetime
class OnboardingChecklist:
"""Manage onboarding checklist."""
def __init__(self, db):
self.db = db
self.checklist_items = [
{
'id': 'profile_complete',
'title': 'Complete Your Profile',
'description': 'Add your name, company, and contact information',
'order': 1,
'required': True
},
{
'id': 'team_invited',
'title': 'Invite Your Team',
'description': 'Add team members to collaborate',
'order': 2,
'required': False
},
{
'id': 'workspace_configured',
'title': 'Configure Your Workspace',
'description': 'Set up your workspace preferences and settings',
'order': 3,
'required': True
},
{
'id': 'integration_connected',
'title': 'Connect Your First Integration',
'description': 'Connect your favorite tools and services',
'order': 4,
'required': False
},
{
'id': 'first_task_completed',
'title': 'Complete Your First Task',
'description': 'Create your first project or task',
'order': 5,
'required': True
}
]
def get_checklist(self, user_id: str) -> dict:
"""Get checklist status for user."""
user = self.db.users.find_one({'id': user_id})
if not user:
return {'error': 'User not found'}
checklist = {
'user_id': user_id,
'items': [],
'completed_count': 0,
'total_count': len(self.checklist_items),
'completion_rate': 0
}
for item in self.checklist_items:
item_status = {
'id': item['id'],
'title': item['title'],
'description': item['description'],
'order': item['order'],
'required': item['required'],
'completed': item['id'] in user.get('completed_steps', [])
}
checklist['items'].append(item_status)
if item_status['completed']:
checklist['completed_count'] += 1
checklist['completion_rate'] = (
checklist['completed_count'] / checklist['total_count'] * 100
)
return checklist
def complete_step(self, user_id: str, step_id: str) -> bool:
"""Mark a step as complete."""
user = self.db.users.find_one({'id': user_id})
if not user:
return False
completed_steps = user.get('completed_steps', [])
if step_id not in completed_steps:
completed_steps.append(step_id)
self.db.users.update_one(
{'id': user_id},
{'$set': {'completed_steps': completed_steps}}
)
return True
Onboarding Email Templates
#!/usr/bin/env python3
"""Onboarding email templates."""
class OnboardingEmailTemplates:
"""Email templates for onboarding."""
def get_welcome_email(self, user_name: str) -> dict:
"""Get welcome email template."""
return {
'subject': f"Welcome to Your SaaS Product, {user_name}!",
'html': f"""
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1>Welcome, {user_name}!</h1>
<p>Thank you for signing up. We're excited to have you on board.</p>
<h2>What's Next?</h2>
<ol>
<li>Complete your profile</li>
<li>Invite your team members</li>
<li>Configure your workspace</li>
<li>Create your first project</li>
</ol>
<p>Need help? Check out our <a href="https://docs.example.com">documentation</a>
or <a href="mailto:[email protected]">contact us</a>.</p>
<p>Best regards,<br>The Team</p>
</div>
""",
'text': f"""
Welcome, {user_name}!
Thank you for signing up. We're excited to have you on board.
What's Next?
1. Complete your profile
2. Invite your team members
3. Configure your workspace
4. Create your first project
Need help? Check out our documentation at https://docs.example.com
or contact us at [email protected].
Best regards,
The Team
"""
}
def get_first_value_email(self, user_name: str) -> dict:
"""Get first value email template."""
return {
'subject': "You've Achieved Your First Value!",
'html': f"""
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1>Congratulations, {user_name}!</h1>
<p>You've completed your first task and achieved your first value with our product!</p>
<p>Here are some tips to get even more value:</p>
<ul>
<li>Invite your team to collaborate</li>
<li>Connect your favorite integrations</li>
<li>Explore our advanced features</li>
</ul>
<p>Keep up the great work!</p>
</div>
""",
'text': f"""
Congratulations, {user_name}!
You've achieved your first value with our product!
Here are some tips to get even more value:
- Invite your team to collaborate
- Connect your favorite integrations
- Explore our advanced features
Keep up the great work!
"""
}
def get_onboarding_complete_email(self, user_name: str) -> dict:
"""Get onboarding complete email template."""
return {
'subject': "Onboarding Complete - You're All Set!",
'html': f"""
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1>Onboarding Complete, {user_name}!</h1>
<p>You've completed all onboarding steps. You're ready to get the most out of our product!</p>
<p>Here are some resources to help you succeed:</p>
<ul>
<li><a href="https://docs.example.com">Documentation</a></li>
<li><a href="https://blog.example.com">Blog</a></li>
<li><a href="https://community.example.com">Community</a></li>
</ul>
<p>Happy creating!</p>
</div>
""",
'text': f"""
Onboarding Complete, {user_name}!
You've completed all onboarding steps. You're ready to get the most out of our product!
Here are some resources to help you succeed:
- Documentation: https://docs.example.com
- Blog: https://blog.example.com
- Community: https://community.example.com
Happy creating!
"""
}
Comments