Skip to main content

Webhooks

Webhooks let Saturday push real-time event notifications to your server. Instead of polling the API for changes, register a webhook URL and Saturday will POST events to you as they happen.

Registering a webhook

import requests

response = requests.post(
    "https://api.saturday.fit/v1/webhooks",
    headers={"Authorization": "Bearer sk_test_abc123def456"},
    json={
        "url": "https://your-app.com/webhooks/saturday",
        "events": [
            "prescription.calculated",
            "athlete.updated",
            "athlete.created",
        ],
    },
)

webhook = response.json()
# Save webhook["secret"] — you need it to verify signatures
print(f"Webhook secret: {webhook['secret']}")
Save the webhook secret immediately. It is only returned once at creation time. You need it to verify that incoming webhooks are genuinely from Saturday.

Available events

EventTriggered when
athlete.createdA new athlete is created
athlete.updatedAn athlete profile is modified
athlete.deletedAn athlete is deleted
activity.createdA new activity is created
activity.updatedAn activity is modified
prescription.calculatedA nutrition prescription is generated
feedback.submittedPost-activity feedback is submitted
subscription.activatedAn athlete subscribes to Saturday
subscription.canceledAn athlete’s subscription is canceled
conversation.message_sentAn AI Coach message is sent

Webhook payload format

Every webhook delivery has this structure:
{
  "id": "evt_abc123def456",
  "type": "prescription.calculated",
  "created_at": "2025-01-15T14:30:00Z",
  "data": {
    "athlete_id": "ath_abc123",
    "activity_id": "act_xyz789",
    "carb_g_per_hr": 60,
      "sodium_mg_per_hr": 500,
      "fluid_ml_per_hr": 600
  }
}

Verifying webhook signatures (HMAC-SHA256)

Every webhook delivery includes a signature in the X-Saturday-Signature header. Always verify this signature to confirm the webhook came from Saturday and wasn’t tampered with. The signature is computed as HMAC-SHA256(webhook_secret, timestamp + "." + raw_body).

Verification steps

  1. Extract the timestamp and signature from the header
  2. Reconstruct the signed payload: {timestamp}.{raw_body}
  3. Compute HMAC-SHA256 using your webhook secret
  4. Compare with constant-time equality
import hashlib
import hmac
import time
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_webhook_secret_here"

@app.route("/webhooks/saturday", methods=["POST"])
def handle_webhook():
    # 1. Extract signature header
    signature_header = request.headers.get("X-Saturday-Signature")
    if not signature_header:
        abort(400, "Missing signature header")

    # 2. Parse timestamp and signature
    parts = signature_header.split(",")
    timestamp = None
    signature = None
    for part in parts:
        key, value = part.split("=", 1)
        if key == "t":
            timestamp = value
        elif key == "v1":
            signature = value

    if not timestamp or not signature:
        abort(400, "Invalid signature format")

    # 3. Reject stale timestamps (5-minute replay window)
    if abs(time.time() - int(timestamp)) > 300:
        abort(400, "Timestamp too old — possible replay attack")

    # 4. Compute expected signature
    raw_body = request.get_data(as_text=True)
    signed_payload = f"{timestamp}.{raw_body}"
    expected = hmac.new(
        WEBHOOK_SECRET.encode("utf-8"),
        signed_payload.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    # 5. Constant-time comparison
    if not hmac.compare_digest(expected, signature):
        abort(400, "Invalid signature")

    # 6. Process the event
    event = request.json
    print(f"Received event: {event['type']}")

    # Always return 200 quickly — process async if needed
    return "", 200

Retry behavior

If your endpoint fails (non-2xx response or timeout), Saturday retries with exponential backoff:
AttemptDelayTotal elapsed
1st retry30 seconds30s
2nd retry2 minutes2.5 min
3rd retry10 minutes12.5 min
4th retry1 hour1 hr 12.5 min
5th retry4 hours5 hr 12.5 min
After 5 failed retries, the delivery is marked as failed and the event is logged for manual retry.

Auto-disable

If a webhook endpoint fails consistently for 3 consecutive days, Saturday automatically disables it and sends a notification email to the partner contact. Re-enable it from the API after fixing the issue:
curl -X PATCH https://api.saturday.fit/v1/webhooks/{webhook_id} \
  -H "Authorization: Bearer sk_live_xyz789..." \
  -d '{"active": true}'

Best practices

  1. Return 200 immediately — process events asynchronously. Saturday times out after 30 seconds.
  2. Handle duplicates — use the id field to deduplicate. The same event may be delivered more than once.
  3. Verify signatures — always. Never trust a webhook payload without HMAC verification.
  4. Use HTTPS — Saturday only delivers to HTTPS endpoints.
  5. Log everything — store raw payloads for debugging. Include the event id in your logs.

Managing webhooks

List webhooks:
GET /v1/webhooks
Update events or URL:
PATCH /v1/webhooks/{webhook_id}
Delete a webhook:
DELETE /v1/webhooks/{webhook_id}