AgentsConnect agents and apps

Send events to an app

An outside system posts events to a route the app serves and signs each one, following Standard Webhooks. The app checks the signature; the platform does not.

Steps

  1. In the app's chat, ask App Builder for a receiver:

    Add POST /api/hooks/findings following Standard Webhooks: signature "v1," +
    base64(HMAC-SHA256(key, f"{id}.{timestamp}.{raw_body}")), where key is the
    base64-decoded app secret RADAR_WEBHOOK_SECRET. Read id, timestamp and signature from
    the query parameters webhook_id, webhook_timestamp and webhook_signature.
    Return 401 for a bad signature or a timestamp more than 5 minutes off. Store
    webhook_id; a repeat returns 200 with the existing row and creates nothing.
  2. Set RADAR_WEBHOOK_SECRET as an app secret: 24 to 64 random bytes, base64-encoded.

  3. Set the app's visibility to Public.

  4. Sign and send each event from the outside system:

    import base64, hashlib, hmac, json, os, time, uuid
    import httpx
    
    key = base64.b64decode(os.environ["RADAR_WEBHOOK_SECRET"])
    body = json.dumps({"title": "Nightly backup failed", "severity": "high"})
    webhook_id, timestamp = f"msg_{uuid.uuid4().hex}", str(int(time.time()))
    signature = base64.b64encode(
        hmac.new(key, f"{webhook_id}.{timestamp}.{body}".encode(), hashlib.sha256).digest()
    ).decode()
    httpx.post(
        "https://<app_id>.<apps domain>/api/hooks/findings",
        content=body,
        params={"webhook_id": webhook_id, "webhook_timestamp": timestamp, "webhook_signature": f"v1,{signature}"},
        headers={"content-type": "application/json"},
    )

    Open in new tab on the app's card shows the app's own address.

Why the signature is in the query string

Standard Webhooks sends webhook-id, webhook-timestamp and webhook-signature as headers. The app gateway does not pass those headers to the app, so the sender puts them in the query string. The secret itself never travels.

Retries

Resend a failed event with the same webhook_id; the app answers with the row it already stored and creates nothing new.

On this page