Subscription Management¶
Subscriptions are the relationships between subscribers and channel packages that determine which channels each subscriber has access to. The subscription system is the key mechanism for monetizing IPTV services in Catena.
What is a Subscription¶
A subscription in Catena is an active link between a subscriber and a channel package. When a subscriber has a subscription to a package, they automatically get access to all channels included in that package.
Key concept:
Subscriber → Subscription → Package → Channels → Viewing
- Subscriber subscribes to one or more packages
- Each package contains a set of channels
- Subscriber gets access to all channels from all their packages
- System checks subscription presence when viewing
Key capabilities:
- Flexible access control — connecting and disconnecting packages in real-time
- Multiple subscriptions — subscriber can be subscribed to multiple packages simultaneously
- Free packages — automatic access to basic content for all subscribers
- Logging — complete history of all subscription changes
- API-first approach — easy integration with billing systems
Typical workflow:
- Billing system receives payment from user
- Billing calls Catena API to create subscription
- Catena immediately grants access to package channels
- Subscriber starts watching channels
- At period end, billing disconnects subscription
- Access to paid channels is automatically blocked
Subscription Lifecycle¶
Creating a Subscription¶
When subscription is created:
- Upon package payment through billing system
- Upon manual connection by administrator
- Upon promo code or bonus activation
- Upon trial period provision
What happens when created:
- Record is created in database about subscriber-package link
- Subscriber immediately gets access to all package channels
- Record is added to operations log (type
createPackageSubscriber
) - On next player request, available channels list is updated
Active Subscription¶
During active subscription:
- Subscriber can watch all package channels without restrictions
- System logs all viewing sessions
- Subscriber's
packages
field contains active package IDs - Streaming server verifies access rights on each stream request
Cancelling a Subscription¶
When subscription is cancelled:
- Upon paid period expiration
- Upon subscription cancellation by user
- Upon subscriber blocking by administrator
- Upon package deletion from system
What happens when cancelled:
- Subscriber-package link record is deleted
- Subscriber immediately loses access to package channels
- Record is added to operations log (type
deletePackageSubscriber
) - Active viewing sessions of package channels are interrupted
Creating a Subscription¶
Via Web Interface¶
- Open subscriber card in "Subscribers" section
- Go to "Subscriptions" or "Packages" tab
- Click "Add Subscription"
- Select package from dropdown list of available packages
- Confirm addition
Subscriber immediately gets access to all channels of the selected package.
Via Management API¶
Create subscriber subscription to package:
curl -X POST https://your-catena-domain.com/tv-management/api/v1/packages-subscribers \
-H "X-Auth-Token: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"subscriberId": "sKl9SW3AAAE.",
"packageId": "pKl9SW3AAAE."
}'
Request parameters:
- subscriberId (required) — ID of subscriber to connect package to
- packageId (required) — ID of package to connect
Response:
{
"subscriberId": "sKl9SW3AAAE.",
"packageId": "pKl9SW3AAAE.",
"portalId": "pKl9SW3AAAE."
}
Important points:
- Subscriber and package must belong to same portal
- If subscription already exists, API will return error
- Changes take effect immediately
- Operation is recorded in log
Deleting a Subscription¶
Via Web Interface¶
- Open subscriber card
- Go to "Subscriptions" tab
- Find package in active subscriptions list
- Click "Delete" or "Disconnect"
- Confirm disconnection
Subscriber immediately loses access to channels of this package.
Via Management API¶
Delete subscriber subscription to package:
curl -X DELETE https://your-catena-domain.com/tv-management/api/v1/packages-subscribers \
-H "X-Auth-Token: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"subscriberId": "sKl9SW3AAAE.",
"packageId": "pKl9SW3AAAE."
}'
Request parameters:
- subscriberId (required) — subscriber ID
- packageId (required) — package ID to disconnect
Response:
HTTP 201 - subscription deleted
Important points:
- If subscription doesn't exist, API will return error
- Active viewing sessions will be interrupted
- Changes take effect immediately
- Operation is recorded in log
Viewing Subscriptions¶
Specific Subscriber Subscriptions¶
Get subscriber's package list:
curl -X GET https://your-catena-domain.com/tv-management/api/v1/subscribers/sKl9SW3AAAE. \
-H "X-Auth-Token: your-api-key"
Response:
{
"subscriberId": "sKl9SW3AAAE.",
"portalId": "pKl9SW3AAAE.",
"name": "John Doe",
"phoneCountryCode": "1",
"phone": "2345678901",
"playback_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"packages": ["pKl9SW3AAAE.", "sportKl9SW3AAAE."]
}
The packages
field contains array of IDs of all packages the subscriber is subscribed to.
Specific Package Subscribers¶
Unfortunately, there's no direct API to get list of package subscribers. Use operations log or get all subscribers and filter by packages
:
# Get all subscribers
curl -X GET https://your-catena-domain.com/tv-management/api/v1/subscribers \
-H "X-Auth-Token: your-api-key" \
| jq '.subscribers[] | select(.packages[] | contains("pKl9SW3AAAE."))'
Subscription History via Operations Log¶
Get all subscription operations for specific subscriber:
curl -X GET "https://your-catena-domain.com/tv-management/api/v1/operations?subscriberId=sKl9SW3AAAE.&type=createPackageSubscriber&type=deletePackageSubscriber" \
-H "X-Auth-Token: your-api-key"
Get operations for specific package:
curl -X GET "https://your-catena-domain.com/tv-management/api/v1/operations?packageId=pKl9SW3AAAE." \
-H "X-Auth-Token: your-api-key"
Response:
{
"operations": [
{
"operationId": "opKl9SW3AAAE.",
"type": "createPackageSubscriber",
"subscriberId": "sKl9SW3AAAE.",
"packageId": "pKl9SW3AAAE.",
"portalId": "pKl9SW3AAAE.",
"createdAt": "2024-10-16T10:05:00Z",
"payload": {
"packageId": "pKl9SW3AAAE.",
"subscriberId": "sKl9SW3AAAE."
}
},
{
"operationId": "opKl9SW3AAAB.",
"type": "deletePackageSubscriber",
"subscriberId": "sKl9SW3AAAE.",
"packageId": "pKl9SW3AAAE.",
"portalId": "pKl9SW3AAAE.",
"createdAt": "2024-10-20T15:30:00Z",
"payload": {
"packageId": "pKl9SW3AAAE.",
"subscriberId": "sKl9SW3AAAE."
}
}
],
"next": "cursor-for-next-page"
}
Operation types:
createPackageSubscriber
— subscription creationdeletePackageSubscriber
— subscription deletionautoCreateSubscriber
— automatic subscriber creation (may include basic package subscription)
Portal Free Packages¶
Catena supports the concept of "free packages" — packages that are automatically available to all portal subscribers without explicit subscription creation.
Free Packages Concept¶
How it works:
- Portal settings define list of free packages
- All portal subscribers automatically get access to channels from these packages
- No need to create individual subscriptions for each subscriber
- Perfect for basic content, demo channels, promotional channels
Use cases:
- Basic content — public channels available to all
- Trial period — demo content for new users
- Promo channels — advertising and informational channels
- Public interest channels — mandatory distribution channels
Managing Free Packages¶
View portal free packages:
curl -X GET https://your-catena-domain.com/tv-management/api/v1/portal \
-H "X-Auth-Token: your-api-key"
Response:
{
"portalId": "pKl9SW3AAAE.",
"name": "my-iptv-portal",
"domain": "iptv.example.com",
"freePackages": ["basicKl9SW3AAAE.", "demoKl9SW3AAAE."],
"branding": {
"title": "My IPTV Service",
"description": "Premium IPTV streaming"
}
}
Add package to free list:
curl -X POST https://your-catena-domain.com/tv-management/api/v1/portal/free-packages/basicKl9SW3AAAE. \
-H "X-Auth-Token: your-api-key"
Remove package from free list:
curl -X DELETE https://your-catena-domain.com/tv-management/api/v1/portal/free-packages/basicKl9SW3AAAE. \
-H "X-Auth-Token: your-api-key"
Important:
- Free package changes apply to all subscribers instantly
- On addition — all subscribers get access to package channels
- On removal — only those without explicit subscription lose access
Billing System Integration¶
Integration Architecture¶
Typical scheme:
[Billing System] ←→ [Catena API] ←→ [Streaming Server]
↓ ↓ ↓
Payments Subscriptions Channel Access
Billing responsibilities:
- Receiving payments from users
- Managing tariffs and subscription periods
- Tracking subscription expirations
- Calling Catena API to connect/disconnect packages
Catena responsibilities:
- Managing channel access
- Verifying rights during viewing
- Logging subscriber activity
- Providing viewing statistics
Integration Examples¶
Example 1: Webhook on Payment¶
Billing sends webhook to your service on successful payment:
from flask import Flask, request
import requests
app = Flask(__name__)
CATENA_API_URL = "https://catena.example.com/tv-management/api/v1"
CATENA_API_KEY = "your-api-key"
@app.route('/billing-webhook', methods=['POST'])
def billing_webhook():
data = request.json
if data['event'] == 'payment.success':
# Payment received - activate subscription
subscriber_id = get_subscriber_id(data['user_phone'])
package_id = get_package_id(data['tariff_name'])
# Create subscription in Catena
response = requests.post(
f"{CATENA_API_URL}/packages-subscribers",
headers={"X-Auth-Token": CATENA_API_KEY},
json={
"subscriberId": subscriber_id,
"packageId": package_id
}
)
if response.status_code == 200:
return {"status": "ok", "message": "Subscription activated"}
else:
return {"status": "error", "message": response.text}, 500
elif data['event'] == 'subscription.expired':
# Subscription expired - deactivate
subscriber_id = get_subscriber_id(data['user_phone'])
package_id = get_package_id(data['tariff_name'])
# Delete subscription in Catena
response = requests.delete(
f"{CATENA_API_URL}/packages-subscribers",
headers={"X-Auth-Token": CATENA_API_KEY},
json={
"subscriberId": subscriber_id,
"packageId": package_id
}
)
return {"status": "ok", "message": "Subscription deactivated"}
return {"status": "ok"}
def get_subscriber_id(phone):
"""Get Catena subscriber ID by phone number"""
response = requests.get(
f"{CATENA_API_URL}/subscribers",
headers={"X-Auth-Token": CATENA_API_KEY}
)
subscribers = response.json()['subscribers']
for sub in subscribers:
full_phone = f"+{sub['phoneCountryCode']}{sub['phone']}"
if full_phone == phone:
return sub['subscriberId']
# If subscriber not found - create
return create_subscriber(phone)
def get_package_id(tariff_name):
"""Map tariff name to package ID"""
tariff_mapping = {
"basic": "basicKl9SW3AAAE.",
"premium": "premiumKl9SW3AAAE.",
"sport": "sportKl9SW3AAAE."
}
return tariff_mapping.get(tariff_name)
if __name__ == '__main__':
app.run(port=5000)
Example 2: Periodic Synchronization¶
Regular checking and synchronization of subscriptions:
import requests
from datetime import datetime, timedelta
CATENA_API_URL = "https://catena.example.com/tv-management/api/v1"
CATENA_API_KEY = "your-api-key"
BILLING_DB = "postgresql://billing_db"
def sync_subscriptions():
"""Synchronize subscriptions between billing and Catena"""
# 1. Get active subscriptions from billing
active_billing_subscriptions = get_active_subscriptions_from_billing()
# 2. Get all Catena subscribers
response = requests.get(
f"{CATENA_API_URL}/subscribers",
headers={"X-Auth-Token": CATENA_API_KEY}
)
catena_subscribers = response.json()['subscribers']
# 3. Compare and synchronize
for billing_sub in active_billing_subscriptions:
phone = billing_sub['phone']
package_id = get_package_id(billing_sub['tariff'])
# Find subscriber in Catena
catena_sub = find_subscriber_by_phone(catena_subscribers, phone)
if catena_sub:
# Check if needed subscription exists
if package_id not in catena_sub['packages']:
# No subscription - create
create_subscription(catena_sub['subscriberId'], package_id)
print(f"Activated: {phone} -> {package_id}")
else:
# No subscriber - create with subscription
create_subscriber_with_package(phone, package_id)
print(f"Created subscriber: {phone}")
# 4. Disconnect expired subscriptions
for catena_sub in catena_subscribers:
phone = f"+{catena_sub['phoneCountryCode']}{catena_sub['phone']}"
for package_id in catena_sub['packages']:
if not has_active_billing_subscription(phone, package_id):
# No subscription in billing - remove from Catena
delete_subscription(catena_sub['subscriberId'], package_id)
print(f"Deactivated: {phone} -> {package_id}")
def create_subscription(subscriber_id, package_id):
requests.post(
f"{CATENA_API_URL}/packages-subscribers",
headers={"X-Auth-Token": CATENA_API_KEY},
json={
"subscriberId": subscriber_id,
"packageId": package_id
}
)
def delete_subscription(subscriber_id, package_id):
requests.delete(
f"{CATENA_API_URL}/packages-subscribers",
headers={"X-Auth-Token": CATENA_API_KEY},
json={
"subscriberId": subscriber_id,
"packageId": package_id
}
)
# Run this function on schedule (e.g., every hour)
if __name__ == '__main__':
sync_subscriptions()
Example 3: Trial Period¶
Automatic trial period for new subscribers:
import requests
from datetime import datetime, timedelta
def activate_trial_subscription(phone, trial_days=7):
"""Activate trial subscription for N days"""
# 1. Create or get subscriber
subscriber_id = get_or_create_subscriber(phone)
# 2. Connect trial package
trial_package_id = "trialKl9SW3AAAE."
response = requests.post(
f"{CATENA_API_URL}/packages-subscribers",
headers={"X-Auth-Token": CATENA_API_KEY},
json={
"subscriberId": subscriber_id,
"packageId": trial_package_id
}
)
if response.status_code == 200:
# 3. Schedule automatic cancellation
schedule_subscription_cancellation(
subscriber_id,
trial_package_id,
datetime.now() + timedelta(days=trial_days)
)
return {
"success": True,
"message": f"Trial activated for {trial_days} days",
"expires_at": (datetime.now() + timedelta(days=trial_days)).isoformat()
}
return {"success": False, "error": response.text}
def schedule_subscription_cancellation(subscriber_id, package_id, cancel_date):
"""Schedule subscription cancellation"""
# Save to task database or use scheduler
# E.g., Celery, APScheduler, or cron job
pass
Typical Use Cases¶
Auto-Renewal Subscription¶
Task: Implement monthly subscription with automatic renewal
Solution:
- Billing charges payment each month
- On successful charge, billing checks subscription presence in Catena
- If subscription exists — do nothing (it's already active)
- If no subscription — create via API
- On failed charge — delete subscription via API
def process_monthly_renewal(user_id, package_name):
"""Process monthly renewal"""
# Charge attempt
payment_success = billing_charge(user_id, get_package_price(package_name))
subscriber_id = get_subscriber_id_by_user(user_id)
package_id = get_package_id(package_name)
if payment_success:
# Payment successful - ensure subscription is active
ensure_subscription_active(subscriber_id, package_id)
else:
# Payment failed - disconnect subscription
deactivate_subscription(subscriber_id, package_id)
send_notification(user_id, "payment_failed")
Family Subscription¶
Task: One payment — access for multiple subscribers (family account)
Solution:
- Create family tariff in billing
- On payment, connect package to all family subscribers
- Store link between subscribers in billing
def activate_family_subscription(family_id, package_name):
"""Activate family subscription"""
package_id = get_package_id(package_name)
# Get all family members from billing
family_members = get_family_members(family_id)
for member in family_members:
subscriber_id = get_subscriber_id(member['phone'])
# Connect package to each
requests.post(
f"{CATENA_API_URL}/packages-subscribers",
headers={"X-Auth-Token": CATENA_API_KEY},
json={
"subscriberId": subscriber_id,
"packageId": package_id
}
)
return {"activated": len(family_members)}
Temporary Promotion¶
Task: Give access to premium channels for the weekend
Solution:
- Friday evening — connect promo package to all active subscribers
- Monday morning — disconnect promo package
#!/bin/bash
# friday-promo.sh - run via cron on Friday at 6 PM
PROMO_PACKAGE_ID="weekendKl9SW3AAAE."
# Get all subscribers
SUBSCRIBERS=$(curl -s -X GET "$CATENA_API_URL/subscribers" \
-H "X-Auth-Token: $CATENA_API_KEY" \
| jq -r '.subscribers[].subscriberId')
# Connect promo package to each
for SUBSCRIBER_ID in $SUBSCRIBERS; do
curl -X POST "$CATENA_API_URL/packages-subscribers" \
-H "X-Auth-Token: $CATENA_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"subscriberId\": \"$SUBSCRIBER_ID\",
\"packageId\": \"$PROMO_PACKAGE_ID\"
}"
done
echo "Promo activated for $(echo "$SUBSCRIBERS" | wc -l) subscribers"
#!/bin/bash
# monday-cleanup.sh - run via cron on Monday at 6 AM
PROMO_PACKAGE_ID="weekendKl9SW3AAAE."
SUBSCRIBERS=$(curl -s -X GET "$CATENA_API_URL/subscribers" \
-H "X-Auth-Token: $CATENA_API_KEY" \
| jq -r '.subscribers[] | select(.packages[] | contains("'$PROMO_PACKAGE_ID'")) | .subscriberId')
for SUBSCRIBER_ID in $SUBSCRIBERS; do
curl -X DELETE "$CATENA_API_URL/packages-subscribers" \
-H "X-Auth-Token: $CATENA_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"subscriberId\": \"$SUBSCRIBER_ID\",
\"packageId\": \"$PROMO_PACKAGE_ID\"
}"
done
echo "Promo deactivated for $(echo "$SUBSCRIBERS" | wc -l) subscribers"
Plan Downgrade¶
Task: Subscriber moves from premium to basic plan
Solution:
def downgrade_subscription(subscriber_id, from_package, to_package):
"""Downgrade subscriber plan"""
from_package_id = get_package_id(from_package)
to_package_id = get_package_id(to_package)
# 1. Disconnect premium package
requests.delete(
f"{CATENA_API_URL}/packages-subscribers",
headers={"X-Auth-Token": CATENA_API_KEY},
json={
"subscriberId": subscriber_id,
"packageId": from_package_id
}
)
# 2. Connect basic package
requests.post(
f"{CATENA_API_URL}/packages-subscribers",
headers={"X-Auth-Token": CATENA_API_KEY},
json={
"subscriberId": subscriber_id,
"packageId": to_package_id
}
)
# 3. Record in billing
billing_record_downgrade(subscriber_id, from_package, to_package)
return {"success": True, "new_package": to_package}
Best Practices¶
Package Design¶
Package structure recommendations:
- Basic package — minimal channel set for all
- Thematic packages — sports, movies, kids, news
- Premium packages — exclusive content, HD/4K channels
- Combo packages — multiple themes in one (savings for subscriber)
Avoid:
- Too many small packages — complicates choice
- Channel duplication between packages — billing confusion
- Package overlaps without logic — one channel in 5 different packages
Error Handling¶
When integrating with billing:
def safe_create_subscription(subscriber_id, package_id, retry_count=3):
"""Create subscription with retries"""
for attempt in range(retry_count):
try:
response = requests.post(
f"{CATENA_API_URL}/packages-subscribers",
headers={"X-Auth-Token": CATENA_API_KEY},
json={
"subscriberId": subscriber_id,
"packageId": package_id
},
timeout=10
)
if response.status_code == 200:
return {"success": True}
elif response.status_code == 409:
# Subscription already exists - this is OK
return {"success": True, "already_exists": True}
else:
# Other error
error_msg = response.json().get('message', 'Unknown error')
log_error(f"Failed to create subscription: {error_msg}")
except requests.exceptions.Timeout:
log_warning(f"Timeout on attempt {attempt + 1}")
if attempt < retry_count - 1:
time.sleep(2 ** attempt) # Exponential backoff
continue
except Exception as e:
log_error(f"Unexpected error: {str(e)}")
break
# All attempts failed - save for manual processing
save_failed_operation("create_subscription", subscriber_id, package_id)
return {"success": False, "error": "Failed after retries"}
State Synchronization¶
Regular data reconciliation:
def audit_subscriptions():
"""Check subscription consistency between systems"""
discrepancies = []
# Get data from both systems
billing_subscriptions = get_billing_subscriptions()
catena_subscriptions = get_catena_subscriptions()
# Find discrepancies
for billing_sub in billing_subscriptions:
if not exists_in_catena(billing_sub, catena_subscriptions):
discrepancies.append({
"type": "missing_in_catena",
"subscriber": billing_sub['phone'],
"package": billing_sub['package']
})
for catena_sub in catena_subscriptions:
if not exists_in_billing(catena_sub, billing_subscriptions):
discrepancies.append({
"type": "missing_in_billing",
"subscriber": catena_sub['phone'],
"package": catena_sub['package']
})
if discrepancies:
# Send notification to administrator
send_audit_report(discrepancies)
# Optionally: auto-fix
auto_fix_discrepancies(discrepancies)
return discrepancies
Logging and Monitoring¶
What to log:
- All subscription creations and deletions
- Errors when calling API
- Catena API response time
- Discrepancies between billing and Catena
Metrics to track:
import prometheus_client as prom
# Prometheus metrics
subscription_creations = prom.Counter(
'catena_subscription_creations_total',
'Total number of subscription creations',
['package_name', 'status']
)
subscription_deletions = prom.Counter(
'catena_subscription_deletions_total',
'Total number of subscription deletions',
['package_name', 'status']
)
api_latency = prom.Histogram(
'catena_api_latency_seconds',
'Latency of Catena API calls',
['endpoint', 'method']
)
def monitored_create_subscription(subscriber_id, package_id):
"""Create subscription with monitoring"""
package_name = get_package_name(package_id)
with api_latency.labels('/packages-subscribers', 'POST').time():
try:
response = requests.post(...)
if response.status_code == 200:
subscription_creations.labels(package_name, 'success').inc()
return {"success": True}
else:
subscription_creations.labels(package_name, 'error').inc()
return {"success": False}
except Exception as e:
subscription_creations.labels(package_name, 'exception').inc()
raise
Subscriber Notifications¶
When to send notifications:
- On subscription activation — "Welcome! Now available channels: ..."
- 3 days before expiration — "Your subscription expires in 3 days"
- On renewal — "Subscription renewed until ..."
- On cancellation — "Subscription cancelled. To renew..."
- On payment error — "Failed to charge. Please check..."
def notify_subscription_activated(subscriber_id, package_name):
"""Notification about subscription activation"""
subscriber = get_subscriber(subscriber_id)
phone = f"+{subscriber['phoneCountryCode']}{subscriber['phone']}"
# Get package channel list
package = get_package(get_package_id(package_name))
channels = ", ".join(package['channels'][:5]) # First 5 channels
message = f"""
🎉 Subscription activated!
Package: {package_name}
Available channels: {channels} and more
Enjoy watching!
"""
send_sms(phone, message)
Troubleshooting¶
Subscription Not Creating¶
Possible causes:
- Invalid
subscriberId
orpackageId
- Subscriber and package from different portals
- Subscription already exists
- API authorization issues
Solution:
- Check subscriber existence:
GET /subscribers/{id}
- Check package existence:
GET /packages/{id}
- Ensure
portalId
matches - Check subscriber's current subscriptions
- Verify API key validity
Subscriber Doesn't See Channels After Subscription Creation¶
Possible causes:
- Package contains no channels
- App didn't update channel list
- Streaming server issues
Solution:
- Check package contents:
GET /packages/{id}
- Ensure package has channels
- Ask subscriber to restart app
- Check subscriber's
playback_token
- Check streaming server logs
Subscription Not Deleting¶
Possible causes:
- Subscription doesn't exist (already deleted)
- Invalid request parameters
- It's a portal free package (can't delete)
Solution:
- Check subscriber's current subscriptions
- Ensure it's not a portal free package
- Verify
subscriberId
andpackageId
correctness - Check operations log for this subscriber
Discrepancies Between Billing and Catena¶
Problem: Subscription active in billing but not in Catena (or vice versa)
Solution:
- Implement regular synchronization (every 15-60 minutes)
- Use operations log to identify issues
- On discrepancy, billing has priority (source of truth)
- Log all changes for analysis
def fix_sync_issue(subscriber_id):
"""Fix desynchronization for subscriber"""
# 1. Get "truth" from billing
billing_packages = get_billing_packages(subscriber_id)
# 2. Get current state in Catena
subscriber = get_catena_subscriber(subscriber_id)
catena_packages = subscriber['packages']
# 3. Synchronize
for package_id in billing_packages:
if package_id not in catena_packages:
# Should be but isn't - add
create_subscription(subscriber_id, package_id)
log_info(f"Fixed: added {package_id} to {subscriber_id}")
for package_id in catena_packages:
if package_id not in billing_packages:
# Exists but shouldn't - remove
delete_subscription(subscriber_id, package_id)
log_info(f"Fixed: removed {package_id} from {subscriber_id}")
See Also¶
- Subscriber Management — creating and configuring subscriber accounts
- Channel Package Management — creating and configuring packages
- Channel Management — adding channels to packages
- Operations Log — tracking all subscription changes
- Portal Configuration — configuring free packages