When you offload desktop automation to a computer use agent, you receive status updates via webhooks. Those updates contain state changes like queued, running, succeeded, failed, or awaiting_human. Without verification, a malicious actor could spoof a run state and cause your downstream systems to react incorrectly. The Coasty Computer Use API signs webhook payloads with an HMAC header so you can verify authenticity.
How webhook signing works
Coasty sends each webhook with the header Coasty-Signature: t=<unix_timestamp>,v1=<hex_signature>. You compute HMAC-SHA256 of the raw request body using your secret key and compare it against v1. The timestamp t helps guard against replay attacks. The header is optional for manual testing and omitted when webhook_url is not set.
import os
import hmac
import hashlib
import json
from datetime import datetime, timezone
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET_KEY = os.getenv("COASTY_API_KEY")
@app.route("/webhook", methods=["POST"])
def webhook():
payload = request.data
signature_header = request.headers.get("Coasty-Signature")
if not signature_header:
return jsonify({"error": "Missing Coasty-Signature"}), 401
# Parse t=<unix>,v1=<hex>
parts = signature_header.split(",")
if len(parts) != 2:
return jsonify({"error": "Invalid signature format"}), 401
timestamp_str, sig_hex = parts
if not timestamp_str.startswith("t=") or not sig_hex.startswith("v1="):
return jsonify({"error": "Invalid signature format"}), 401
timestamp = int(timestamp_str[2:])
received_sig = sig_hex[3:]
# Replay protection: reject if more than 300 seconds old
now = int(datetime.now(timezone.utc).timestamp())
if abs(now - timestamp) > 300:
return jsonify({"error": "Timestamp too old"}), 401
# Compute expected HMAC
expected_sig = hmac.new(
SECRET_KEY.encode("utf-8"),
payload,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(received_sig, expected_sig):
return jsonify({"error": "Invalid signature"}), 401
# Valid payload
data = json.loads(payload.decode("utf-8"))
run_id = data.get("run_id")
status = data.get("status")
print(f"Run {run_id} is now {status}")
return jsonify({"accepted": True, "run_id": run_id, "status": status}), 200
if __name__ == "__main__":
app.run(port=8080)Webhook payload structure
- Each webhook contains a JSON body with run_id and status.
- Status can be queued, running, awaiting_human, succeeded, failed, cancelled, or timed_out.
- Coasty generates these updates when you create a task run with POST /v1/runs or trigger a workflow step.
- If you set a webhook_url on POST /v1/runs, Coasty sends a signed payload for every status change.
- If you omit webhook_url, you must poll GET /v1/runs/{id} for state, but you still benefit from signed events when webhooks are configured.
Always verify Coasty-Signature: t=<unix>,v1=<hex> before processing a webhook payload.
Where this beats brittle automation
Traditional automation relies on fragile selectors, XPath, or detailed element IDs that change when a UI is updated. The Coasty Computer Use API lets you drive the real desktop environment, observe the screen, and act like a human. Webhooks give you a reliable, low-latency way to react to run state changes without polling. With HMAC verification, you can be confident that a status update truly comes from Coasty, not from a forged request, keeping your orchestration logic safe.
Add HMAC signature verification to your webhook handlers to safely handle task runs and workflow events from the Coasty Computer Use API. Start building secure automation pipelines today at https://coasty.ai/developers.
Want to see this in action?
View Case Studies