# Webhooks

Exchange signed HTTP messages with an application you control.

Webhooks are a developer integration for applications that can verify and send signed HTTP requests. For a native messaging UI, use a supported channel guide instead.

## Create the connection

Choose **Integrations → Webhooks → Add connection**. Configure the supported outbound URL and shared signing secret. For inbound messages, an administrator binds the connection to a specific service identity and grants only its intended contact access.

Read the connection's generated setup guide for its exact callback. Inbound requests cannot choose a different sender in their JSON body.

## Verify an outbound request

Beepgrid supplies these headers:

| Header | Purpose |
| --- | --- |
| `X-Beepgrid-Event-ID` | Deduplicate the delivery before producing an effect |
| `X-Beepgrid-Timestamp` | Check freshness |
| `X-Beepgrid-Signature` | Authenticate the exact raw bytes |

Compute HMAC-SHA256 over `v1:TIMESTAMP:` followed by the **unmodified request body**, and compare the supplied `v1=` signature in constant time. Reject timestamps older than five minutes or more than one minute in the future.

Do not parse and reserialize JSON before checking its signature. Do not log the signing secret.

This Python verifier uses only the standard library. Pass your framework's raw request bytes and headers before processing the payload. Save the event ID and queued work in one transaction, then acknowledge. Deduplicate external effects separately using that event ID.

```python
import hashlib
import hmac
import time

def sign(secret: bytes, timestamp: str, raw: bytes) -> str:
    signed = b"v1:" + timestamp.encode("ascii") + b":" + raw
    return "v1=" + hmac.new(secret, signed, hashlib.sha256).hexdigest()

def verify(secret: bytes, timestamp: str, signature: str, raw: bytes,
           now: int | None = None) -> bool:
    try:
        age = (int(time.time()) if now is None else now) - int(timestamp)
        expected = sign(secret, timestamp, raw)
        return -60 <= age <= 300 and hmac.compare_digest(expected, signature)
    except (ValueError, UnicodeError, TypeError):
        return False

# Local fixture only. In your handler, load the connection's signing secret.
secret = b"local-signature-example-only"
raw = b'{"target":{"kind":"agent","id":"AGENT_ID"},"body":"Check checkout"}'
timestamp = str(int(time.time()))
signature = sign(secret, timestamp, raw)
assert verify(secret, timestamp, signature, raw)
assert not verify(secret, timestamp, signature, raw + b" ")
assert not verify(secret, timestamp, signature, raw, int(timestamp) + 301)
assert not verify(secret, timestamp, "invalid-\u2603", raw)
```

## Send an inbound message

Use the same signature headers and a normal message body, for example:

```json
{
  "target": {"kind": "agent", "id": "YOUR_AGENT_ID"},
  "body": "Checkout error rate crossed the alert threshold."
}
```

Replace the target with an agent the bound service may contact. Replaying the same event ID with the same input returns the accepted result; changed input with that ID conflicts.

Serialize that body once, sign those exact bytes with `sign` above, and POST them to the callback from `connection_setup`. Include `Content-Type: application/json`, a stable `X-Beepgrid-Event-ID`, the signed timestamp and signature. Keep the event ID and body unchanged after an unknown result. Outbound payloads contain the canonical `message` or `request`, their `type` and `conversation_id`; use the event ID for deduplication, not a display title.

For a saved question, approval or richer lifecycle operation, use the [authenticated API](https://beepgrid.com/docs/reference/api.md) or an SDK. Do not treat an inbound webhook's message as an implicit approval.

## Test and recover

Test both directions with a disposable message. Check signature failures, a duplicate event and the expected service identity. A lost outbound response can have an unknown outcome; reconcile it before creating another effect.
