Getting Started with Webhooks for Real-Time Marine Weather Alerts

Back to Blog

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:

  1. Your app creates a SpotCast with a webhook_url
  2. SeaLegs processes the forecast (fetches weather models, runs AI analysis)
  3. When complete, SeaLegs POSTs the result to your webhook URL
  4. 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:

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:

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

What's Next

Once you have webhooks working, you can build on top of them:

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.

Related Posts

Jan 10, 2026

Building with the SpotCast API

A deep dive into creating location-specific marine forecasts, async processing, and vessel customization.

Ready to Build with Marine Weather Data?

Get started with the SeaLegs API and bring AI-powered marine forecasts to your users.