Polling works fine when you're making one-off forecast requests. But if you're building a system that monitors conditions for dozens or hundreds of locations — say, a daily alerting service for marina customers or a fleet tracking tool — you don't want to be polling status endpoints in a loop. That's what webhooks are for.
When a SpotCast forecast completes, SeaLegs sends an HTTP POST to your server with the result. No polling, no wasted requests. In this tutorial, we'll build a complete webhook receiver that processes forecasts, verifies authenticity, and routes alerts based on conditions.
How Webhooks Fit In
The flow looks like this:
- Your app creates a SpotCast with a
webhook_url - SeaLegs processes the forecast (fetches weather models, runs AI analysis)
- When complete, SeaLegs POSTs the result to your webhook URL
- Your server processes the payload and takes action (send alert, update UI, etc.)
The entire round trip typically takes a few seconds. From your server's perspective, it's just an incoming HTTP request with a JSON payload.
Step 1: Create a Webhook Endpoint
You need an HTTPS endpoint that accepts POST requests. Here's a minimal example with Flask:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/api/weather-webhook", methods=["POST"])
def weather_webhook():
payload = request.json
event = request.headers.get("X-SeaLegs-Event")
if event == "spotcast.forecast.completed":
data = payload["data"]
print(f"Forecast ready: {data['spotcast_id']}")
print(f"Summary: {data['summary']}")
# Route based on conditions
process_forecast(data)
elif event == "spotcast.forecast.failed":
data = payload["data"]
print(f"Forecast failed: {data['error']['message']}")
return jsonify({"received": True}), 200
Requirement: Your webhook URL must use HTTPS. SeaLegs will not deliver webhooks to HTTP endpoints.
Step 2: Trigger a Forecast with a Webhook
Include the webhook_url when creating a SpotCast. Add metadata to carry context you'll need when the webhook fires:
curl -X POST https://api.sealegs.ai/v3/spotcast \
-H "X-API-Key: your_api_key" \
-d '{
"latitude": 40.461,
"longitude": -73.577,
"start_date": "2026-02-03",
"num_days": 3,
"webhook_url": "https://yourapp.com/api/weather-webhook",
"metadata": {
"user_id": "usr_123",
"location_name": "Hudson Canyon",
"alert_type": "daily_briefing"
}
}'
The API returns immediately with the SpotCast ID and pending status. A few seconds later, your webhook endpoint receives the completed forecast.
Step 3: Handle the Webhook Payload
The spotcast.forecast.completed payload looks like this:
{
"event": "spotcast.forecast.completed",
"data": {
"spotcast_id": "spc_abc123",
"forecast_id": "fcst_xyz789",
"status": "completed",
"summary": "Mixed conditions over the three-day period.
Tuesday shows CAUTION with 18-22kt winds and 4-5ft
seas. Wednesday and Thursday improve significantly
with GO conditions, light winds (8-12kt), and
manageable 2ft seas.",
"metadata": {
"user_id": "usr_123",
"location_name": "Hudson Canyon",
"alert_type": "daily_briefing"
}
}
}
Your metadata comes back exactly as you sent it, so you can immediately route the alert to the right user and location without a database lookup.
Step 4: Verify the Signature
In production, you should verify that webhooks are actually coming from SeaLegs, not from someone spoofing requests to your endpoint. Every webhook includes a signature header:
X-SeaLegs-Signature— HMAC-SHA256 hash of the request bodyX-SeaLegs-Timestamp— Unix timestamp of when the webhook was sentX-SeaLegs-Delivery-ID— Unique ID for deduplication (format:whk_...)
Here's how to verify the signature in Python:
import hmac
import hashlib
WEBHOOK_SECRET = "your_webhook_secret" # from your dashboard
def verify_signature(request):
signature = request.headers.get("X-SeaLegs-Signature")
if not signature:
return False
expected = hmac.new(
WEBHOOK_SECRET.encode(),
request.data, # raw request body bytes
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Add this check to your webhook handler:
@app.route("/api/weather-webhook", methods=["POST"])
def weather_webhook():
if not verify_signature(request):
return jsonify({"error": "Invalid signature"}), 401
# ... process the webhook
return jsonify({"received": True}), 200
Security: Your webhook secret is available in your Developer Dashboard. Keep it secret — don't commit it to version control or expose it in client-side code.
Step 5: Handle Retries and Deduplication
If your endpoint returns a non-2xx status code or doesn't respond within 10 seconds, SeaLegs retries with exponential backoff:
- 1st retry: 5 minutes
- 2nd retry: 30 minutes
- 3rd retry: 2 hours
- 4th retry: 24 hours
After 5 consecutive failures, the webhook is marked as failed and no more retries are attempted for that delivery.
Because of retries, your handler might receive the same webhook more than once. Use the X-SeaLegs-Delivery-ID header to detect duplicates:
processed_deliveries = set() # use Redis or a DB in production
@app.route("/api/weather-webhook", methods=["POST"])
def weather_webhook():
if not verify_signature(request):
return jsonify({"error": "Invalid signature"}), 401
delivery_id = request.headers.get("X-SeaLegs-Delivery-ID")
if delivery_id in processed_deliveries:
return jsonify({"received": True}), 200 # already handled
processed_deliveries.add(delivery_id)
# ... process the webhook
return jsonify({"received": True}), 200
Building a Daily Alert System
Now let's put it together into something practical: a daily marine weather alert for a list of saved locations. Every morning, your system creates forecasts for each location, and the webhooks route the results to users as push notifications or emails.
The Scheduler (runs daily at 6 AM)
import requests
from datetime import date
API_KEY = "your_api_key"
WEBHOOK_URL = "https://yourapp.com/api/weather-webhook"
# Users and their saved locations (from your database)
user_locations = [
{"user_id": "usr_123", "name": "Hudson Canyon",
"lat": 40.461, "lon": -73.577},
{"user_id": "usr_123", "name": "Montauk Point",
"lat": 41.071, "lon": -71.857},
{"user_id": "usr_456", "name": "Cape May Inlet",
"lat": 38.948, "lon": -74.869},
]
for loc in user_locations:
requests.post(
"https://api.sealegs.ai/v3/spotcast",
headers={"X-API-Key": API_KEY},
json={
"latitude": loc["lat"],
"longitude": loc["lon"],
"start_date": str(date.today()),
"num_days": 3,
"webhook_url": WEBHOOK_URL,
"metadata": {
"user_id": loc["user_id"],
"location_name": loc["name"],
"alert_type": "daily_briefing"
}
}
)
The Webhook Handler (routes alerts)
@app.route("/api/weather-webhook", methods=["POST"])
def weather_webhook():
if not verify_signature(request):
return jsonify({"error": "Invalid signature"}), 401
delivery_id = request.headers.get("X-SeaLegs-Delivery-ID")
if is_duplicate(delivery_id):
return jsonify({"received": True}), 200
payload = request.json
event = request.headers.get("X-SeaLegs-Event")
if event == "spotcast.forecast.completed":
data = payload["data"]
user_id = data["metadata"]["user_id"]
location = data["metadata"]["location_name"]
summary = data["summary"]
# Send the alert
send_push_notification(
user_id=user_id,
title=f"Marine forecast: {location}",
body=summary
)
return jsonify({"received": True}), 200
That's the whole system. The scheduler fires 3 API calls and your webhook handler sends 3 push notifications when the forecasts are ready. No polling loops, no cron jobs checking for results. Scale it to 100 locations and the pattern is the same — you just get 100 webhook deliveries instead of 3. This is how operators monitor conditions across entire regions like the Gulf of Mexico or Northeast coast.
Testing Locally
During development, your localhost isn't reachable from the internet. Use a tunneling tool like ngrok to expose your local webhook endpoint:
# Terminal 1: run your Flask app
flask run --port 5000
# Terminal 2: expose it via ngrok
ngrok http 5000
Ngrok gives you a public HTTPS URL like https://abc123.ngrok.io. Use that as your webhook_url during development. The webhooks will tunnel through to your local server.
Best Practices
- Return 200 quickly. Do your heavy processing (sending emails, updating databases) asynchronously. If your handler takes too long, the webhook will time out and trigger a retry.
- Always verify signatures in production. It's one function call and prevents spoofed requests from triggering actions in your system.
- Store the delivery ID before processing. If your handler crashes mid-processing and the webhook retries, you'll know whether you've already started handling it.
- Use metadata generously. Attach user IDs, trip IDs, alert preferences — anything you'll need when the webhook arrives. It's free storage that travels with the forecast.
- Handle failures gracefully. If a
spotcast.forecast.failedevent comes in, don't just log it — notify the user that their forecast couldn't be generated and suggest they try again.
What's Next
Once you have webhooks working, you can build on top of them:
- Conditional alerts — only notify users when conditions are CAUTION or AVOID, not on GO days
- Multi-channel delivery — route to email, SMS, Slack, or push notifications based on user preferences
- Forecast comparison — trigger forecasts for multiple locations and send a "best spot this weekend" summary
- Historical tracking — store every webhook payload and build trend analysis over time
Check out the full webhook documentation for the complete specification, or read about the SpotCast API if you're just getting started with forecasts.
Ready to build? Create your free developer account and start receiving marine weather alerts in minutes.