Webhooks
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
Section titled “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
Section titled “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.
import hashlibimport hmacimport 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
Section titled “Send an inbound message”Use the same signature headers and a normal message body, for example:
{ "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 or an SDK. Do not treat an inbound webhook’s message as an implicit approval.
Test and recover
Section titled “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.