Engineering

Verifying Computer Use API Webhooks with HMAC Signatures

Lisa Chen||5 min
Alt+F4

When you POST /v1/runs to start a computer use agent, you can provide a webhook_url. Coasty sends POST requests containing JSON events for each state change. To trust the payload, you must verify the HMAC v1 signature header Coasty-Signature. This guide shows how to decode that header, compute the MAC, and reject tampered payloads.

How webhook signatures work

Coasty signs every webhook payload with HMAC-SHA256 using a shared secret from your developer key. The header format is Coasty-Signature: t={unix_timestamp},v1={hex_mac}. Decode the header, extract timestamp and hex MAC, then recompute the signature using the raw body and the secret. Reject requests where t is too old (clock skew) or MACs differ.

python
import os
import json
import hmac
import hashlib
from datetime import datetime, timedelta

COASTY_API_KEY = os.getenv('COASTY_API_KEY')

COASTY_SIGNATURE_HEADER = 'Coasty-Signature'

def verify_webhook_signature(raw_body: bytes, signature_header: str) -> bool:
    if not signature_header:
        return False

    # Expected header format: Coasty-Signature: t=1234567890,v1=a1b2c3...
    parts = signature_header.split(',')
    timestamp, mac_hex = None, None
    for part in parts:
        if part.startswith('t='):
            timestamp = int(part[2:])
        elif part.startswith('v1='):
            mac_hex = part[3:]

    if not (timestamp and mac_hex):
        return False

    # Reject messages older than 5 minutes
    if datetime.utcnow() - timedelta(seconds=timestamp) > timedelta(minutes=5):
        return False

    # Recompute HMAC-SHA256 of raw body using the API key
    mac = hmac.new(COASTY_API_KEY.encode('utf-8'), raw_body, hashlib.sha256)
    expected_mac = mac.hexdigest()

    # Constant-time comparison to prevent timing leaks
    return hmac.compare_digest(expected_mac, mac_hex)

# Example webhook handler
async def handle_webhook(request):
    raw_body = await request.body()
    signature_header = request.headers.get(COASTY_SIGNATURE_HEADER)

    if not verify_webhook_signature(raw_body, signature_header):
        # Reject
        return json.dumps({"error": "Invalid signature"}), 401

    payload = json.loads(raw_body)
    run_id = payload.get('run_id')
    state = payload.get('state')
    print(f'Run {run_id} is now {state}')

    return json.dumps({"status": "acknowledged"}), 200

Key webhook fields and costs

  • POST /v1/runs accepts webhook_url. The server triggers webhooks on state changes.
  • Webhook payload JSON includes run_id, state (queued, running, awaiting_human, succeeded, failed, cancelled, timed_out).
  • Each agent step is billed $0.05. Webhook delivery is free.
  • Use Idempotency-Key header on writes for safe retries.
  • Webhooks are HMAC signed with header Coasty-Signature: t={unix},v1={hex}.

Always reject webhooks with invalid or stale Coasty-Signature headers.

Where this beats brittle automation

Browser automation often relies on fragile selectors that break when a layout changes. A computer use agent perceives the screen and executes actions like a human, making it resilient to UI shifts. Webhooks let you run serverless workflows that react only to real state changes and not to polling or spurious events, keeping your system lean and correct.

You can now build secure, event-driven pipelines that trust Coasty webhook payloads. Hook into task run state changes, trigger downstream steps, and orchestrate workflows. Get a developer key and start building at https://coasty.ai/developers.

Want to see this in action?

View Case Studies
Try Coasty Free