.claude/skills/terra-webhooks/SKILL.md
Terra API webhook handling for real-time health data. Use when setting up webhook endpoints, verifying signatures, handling events, or debugging webhook issues.
npx skillsauth add adaptationio/skrillz terra-webhooksInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Handle real-time health data delivery from Terra API.
from flask import Flask, request
import hmac
import hashlib
app = Flask(__name__)
TERRA_SIGNING_SECRET = "your_signing_secret_from_dashboard"
@app.route("/webhooks/terra", methods=["POST"])
def handle_terra_webhook():
# 1. Verify signature
signature = request.headers.get("terra-signature")
if not verify_signature(signature, request.get_data()):
return "Invalid signature", 401
# 2. Parse payload
payload = request.get_json()
event_type = payload.get("type")
# 3. Handle event
if event_type == "activity":
handle_activity(payload)
elif event_type == "sleep":
handle_sleep(payload)
elif event_type == "auth":
handle_user_connected(payload)
# 4. Respond immediately
return "OK", 200
def verify_signature(header: str, body: bytes) -> bool:
"""Verify Terra webhook signature."""
parts = dict(p.split("=") for p in header.split(","))
timestamp = parts["t"]
signature = parts["v1"]
message = f"{timestamp}.{body.decode()}"
expected = hmac.new(
TERRA_SIGNING_SECRET.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
| Event | Description |
|-------|-------------|
| auth | User successfully connected |
| deauth | User disconnected |
| user_reauth | User re-authenticated |
| access_revoked | Provider revoked access |
| connection_error | Connection failed |
| Event | Description |
|-------|-------------|
| activity | New workout/activity data |
| sleep | New sleep session data |
| body | Body metrics update |
| daily | Daily summary update |
| nutrition | Nutrition/meal data |
| menstruation | Cycle tracking data |
| athlete | User profile update |
| Event | Description |
|-------|-------------|
| processing | Data is being processed |
| large_request_processing | Large request in progress |
| large_request_sending | Large request sending chunks |
auth - User Connected{
"type": "auth",
"user": {
"user_id": "terra_abc123",
"provider": "FITBIT",
"reference_id": "user_12345",
"scopes": ["activity", "sleep", "body"]
},
"status": "authenticated"
}
activity - Workout Data{
"type": "activity",
"user": {
"user_id": "terra_abc123",
"provider": "GARMIN",
"reference_id": "user_12345"
},
"data": [{
"metadata": {
"start_time": "2025-12-05T07:00:00Z",
"end_time": "2025-12-05T08:00:00Z",
"type": "running"
},
"calories_data": {
"total_burned_calories": 450
},
"heart_rate_data": {
"summary": { "avg_hr_bpm": 145, "max_hr_bpm": 175 }
},
"distance_data": { "distance_meters": 8500 }
}]
}
sleep - Sleep Data{
"type": "sleep",
"user": {
"user_id": "terra_abc123",
"provider": "OURA"
},
"data": [{
"metadata": {
"start_time": "2025-12-04T22:30:00Z",
"end_time": "2025-12-05T06:30:00Z"
},
"sleep_durations_data": {
"sleep_efficiency": 0.92
},
"asleep": {
"duration_deep_sleep_state_seconds": 5400,
"duration_REM_sleep_state_seconds": 6600
}
}]
}
daily - Daily Summary{
"type": "daily",
"user": {
"user_id": "terra_abc123",
"provider": "FITBIT"
},
"data": [{
"metadata": {
"start_time": "2025-12-05T00:00:00Z",
"end_time": "2025-12-05T23:59:59Z"
},
"movement_data": { "steps_count": 10500 },
"calories_data": { "total_burned_calories": 2400 }
}]
}
deauth - User Disconnected{
"type": "deauth",
"user": {
"user_id": "terra_abc123",
"provider": "FITBIT"
},
"status": "deauthenticated"
}
setup-webhook-endpointCreate a production-ready webhook handler.
from flask import Flask, request
from celery import Celery
import hmac
import hashlib
import logging
app = Flask(__name__)
celery = Celery()
logger = logging.getLogger(__name__)
TERRA_SIGNING_SECRET = "your_signing_secret"
# Terra webhook source IPs (for additional security)
TERRA_IPS = [
"18.133.218.210", "18.169.82.189", "18.132.162.19",
"18.130.218.186", "13.43.183.154", "3.11.208.36",
"35.214.201.105", "35.214.230.71", "35.214.252.53", "35.214.229.114"
]
@app.route("/webhooks/terra", methods=["POST"])
def terra_webhook():
# Optional: IP whitelist check
client_ip = request.remote_addr
if client_ip not in TERRA_IPS:
logger.warning(f"Webhook from unknown IP: {client_ip}")
# Consider: return "Forbidden", 403
# Verify signature
signature = request.headers.get("terra-signature")
raw_body = request.get_data()
if not signature or not verify_signature(signature, raw_body):
logger.error("Invalid webhook signature")
return "Invalid signature", 401
# Parse and queue for async processing
payload = request.get_json()
process_webhook.delay(payload)
# Respond immediately (within 5 seconds)
return "OK", 200
def verify_signature(header: str, body: bytes) -> bool:
"""HMAC-SHA256 signature verification."""
try:
parts = dict(p.split("=") for p in header.split(","))
timestamp = parts["t"]
signature = parts["v1"]
message = f"{timestamp}.{body.decode()}"
expected = hmac.new(
TERRA_SIGNING_SECRET.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
# Constant-time comparison to prevent timing attacks
return hmac.compare_digest(expected, signature)
except Exception as e:
logger.error(f"Signature verification error: {e}")
return False
@celery.task
def process_webhook(payload: dict):
"""Async webhook processing."""
event_type = payload.get("type")
user = payload.get("user", {})
logger.info(f"Processing {event_type} for user {user.get('user_id')}")
handlers = {
"auth": handle_auth,
"deauth": handle_deauth,
"activity": handle_activity,
"sleep": handle_sleep,
"body": handle_body,
"daily": handle_daily,
"nutrition": handle_nutrition,
}
handler = handlers.get(event_type)
if handler:
handler(payload)
else:
logger.warning(f"Unknown event type: {event_type}")
handle-data-eventsProcess incoming health data.
def handle_activity(payload: dict):
"""Handle activity/workout data."""
user_id = payload["user"]["user_id"]
for activity in payload.get("data", []):
metadata = activity["metadata"]
unique_key = f"{user_id}:{metadata['start_time']}:{metadata['end_time']}"
# Insert if not exists (activities are unique sessions)
db.activities.update_one(
{"_id": unique_key},
{"$setOnInsert": activity},
upsert=True
)
logger.info(f"Processed activity: {metadata['type']}")
def handle_daily(payload: dict):
"""Handle daily summary data."""
user_id = payload["user"]["user_id"]
for daily in payload.get("data", []):
date = daily["metadata"]["start_time"][:10] # YYYY-MM-DD
# UPSERT - daily data updates multiple times per day
db.daily.update_one(
{"user_id": user_id, "date": date},
{"$set": daily},
upsert=True
)
logger.info(f"Updated daily for {date}")
def handle_sleep(payload: dict):
"""Handle sleep data."""
user_id = payload["user"]["user_id"]
for sleep in payload.get("data", []):
metadata = sleep["metadata"]
unique_key = f"{user_id}:{metadata['start_time']}:{metadata['end_time']}"
db.sleep.update_one(
{"_id": unique_key},
{"$setOnInsert": sleep},
upsert=True
)
def handle_body(payload: dict):
"""Handle body metrics data."""
user_id = payload["user"]["user_id"]
for body in payload.get("data", []):
date = body["metadata"]["start_time"][:10]
# UPSERT - body data updates multiple times per day
db.body.update_one(
{"user_id": user_id, "date": date},
{"$set": body},
upsert=True
)
handle-auth-eventsProcess connection lifecycle events.
def handle_auth(payload: dict):
"""Handle new user connection."""
user = payload["user"]
# Store Terra user mapping
db.terra_users.insert_one({
"terra_user_id": user["user_id"],
"provider": user["provider"],
"reference_id": user["reference_id"],
"scopes": user.get("scopes", []),
"connected_at": datetime.now(),
"status": "active"
})
# Trigger historical data backfill
trigger_backfill.delay(user["user_id"])
logger.info(f"User connected: {user['user_id']} via {user['provider']}")
def handle_deauth(payload: dict):
"""Handle user disconnection."""
user = payload["user"]
# Mark as disconnected
db.terra_users.update_one(
{"terra_user_id": user["user_id"]},
{"$set": {"status": "disconnected", "disconnected_at": datetime.now()}}
)
logger.info(f"User disconnected: {user['user_id']}")
verify-signatureSignature verification utility.
import hmac
import hashlib
def verify_terra_signature(
signature_header: str,
raw_body: bytes,
signing_secret: str
) -> bool:
"""
Verify Terra webhook signature.
Header format: terra-signature: t=1234567890,v1=abc123...
Args:
signature_header: The terra-signature header value
raw_body: Raw request body (bytes)
signing_secret: Your signing secret from Terra dashboard
Returns:
bool: True if signature is valid
"""
try:
# Parse header
parts = {}
for part in signature_header.split(","):
key, value = part.split("=", 1)
parts[key] = value
timestamp = parts["t"]
signature = parts["v1"]
# Compute expected signature
message = f"{timestamp}.{raw_body.decode()}"
expected = hmac.new(
signing_secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(expected, signature)
except Exception:
return False
Terra retries failed webhooks:
| Attempt | Delay | |---------|-------| | 1 | Immediate | | 2 | ~30 seconds | | 3 | ~2 minutes | | 4 | ~10 minutes | | 5 | ~30 minutes | | 6 | ~2 hours | | 7 | ~8 hours | | 8 | ~24 hours |
Total: ~8 retries over 24+ hours
Failure conditions:
Handle duplicate webhooks safely:
def handle_webhook_idempotent(payload: dict):
"""Process webhook with idempotency."""
# Generate idempotency key
user_id = payload["user"]["user_id"]
event_type = payload["type"]
if event_type in ["activity", "sleep"]:
# Session-based: use start+end time
data = payload["data"][0]
key = f"{user_id}:{data['metadata']['start_time']}:{data['metadata']['end_time']}"
elif event_type in ["daily", "body"]:
# Date-based: use date
data = payload["data"][0]
key = f"{user_id}:{data['metadata']['start_time'][:10]}"
else:
# Auth events: use user_id + type + timestamp
key = f"{user_id}:{event_type}:{datetime.now().isoformat()}"
# Check if already processed
if db.processed_webhooks.find_one({"_id": key}):
logger.info(f"Duplicate webhook skipped: {key}")
return
# Process and mark as done
process_event(payload)
db.processed_webhooks.insert_one({"_id": key, "processed_at": datetime.now()})
# Install ngrok
npm install -g ngrok
# Start your server
python app.py # Running on localhost:5000
# Expose with ngrok
ngrok http 5000
# Use ngrok URL in Terra dashboard
# https://abc123.ngrok.io/webhooks/terra
# Simulate webhook (without signature)
curl -X POST http://localhost:5000/webhooks/terra \
-H "Content-Type: application/json" \
-d '{
"type": "activity",
"user": {"user_id": "test123", "provider": "FITBIT"},
"data": [{"metadata": {"type": "running"}}]
}'
Terra webhooks come from these IPs:
TERRA_IPS = [
"18.133.218.210",
"18.169.82.189",
"18.132.162.19",
"18.130.218.186",
"13.43.183.154",
"3.11.208.36",
"35.214.201.105",
"35.214.230.71",
"35.214.252.53",
"35.214.229.114"
]
development
Setup secure web-based terminal access to WSL2 from mobile/tablet via ttyd + ngrok/Cloudflare/Tailscale. One-command install, start, stop, status. Use when you need remote terminal access, web terminal, browser-based shell, or mobile access to WSL2 environment.
development
Complete development workflows where Claude writes the code while Gemini and Codex provide research, planning, reviews, and different perspectives. Claude remains the main developer. Use for complex projects requiring expert planning and multi-perspective reviews.
development
Systematic progress tracking for skill development. Manages task states (pending/in_progress/completed), updates in real-time, reports progress, identifies blockers, and maintains momentum. Use when tracking skill development, coordinating work, or reporting progress.
testing
Comprehensive testing workflow orchestrating functional testing, example validation, integration testing, and usability assessment. Sequential workflow for complete skill testing from examples through scenarios to integration validation. Use when conducting thorough testing, pre-deployment validation, ensuring skill functionality, or comprehensive quality checks.