Webhooks
Receive real-time HTTP callbacks when events occur in your Opengram instance.
Webhooks let your backend receive HTTP POST notifications when specific events occur in Opengram. This is useful for logging, triggering external workflows, or integrating with systems that cannot poll the API.
Configuration
Define webhooks in the hooks array of opengram.config.json. Each hook object supports:
| Field | Required | Default | Description |
|---|---|---|---|
url | Yes | -- | The endpoint Opengram will POST to. |
events | Yes | -- | Array of event names to subscribe to. |
headers | No | {} | Extra headers to include in the request. |
signingSecret | No | -- | Secret used to sign payloads for verification. |
timeoutMs | No | 5000 | Request timeout in milliseconds. |
maxRetries | No | 3 | Maximum number of retry attempts on failure. |
Example
{
"hooks": [
{
"url": "https://api.example.com/opengram/webhook",
"events": ["message.created", "request.resolved"],
"signingSecret": "whsec_your_secret_here",
"headers": {
"X-Custom-Header": "my-value"
},
"timeoutMs": 10000,
"maxRetries": 5
}
]
}Supported events
Messages
| Event | Description |
|---|---|
message.created | A new message was created in a chat (by a user or an agent). |
message.streaming.complete | An agent message finished streaming (or was cancelled mid-stream). |
Requests
| Event | Description |
|---|---|
request.created | A new interactive request was created. |
request.resolved | An interactive request was resolved by the user. |
request.cancelled | An interactive request was cancelled. |
Chats
| Event | Description |
|---|---|
chat.created | A new chat was created. |
chat.updated | A chat's title, tags, or other metadata changed. |
chat.archived | A chat was archived. |
chat.unarchived | A chat was unarchived. |
chat.read | A chat was marked as read. |
chat.unread | A chat was marked as unread. |
Media
| Event | Description |
|---|---|
media.attached | A media file was attached to a chat. |
Payload
Each webhook delivery is an HTTP POST with a JSON body. Every delivery is wrapped in an event envelope:
{
"id": "evt_abc123",
"type": "message.created",
"timestamp": "2025-03-01T19:00:00.000Z",
"payload": { ... }
}The payload fields vary by event type. For message.created and request.* events, Opengram enriches the payload with additional data so your handler does not need to make extra API calls. Other events pass through the raw payload as emitted.
message.created
{
"chatId": "chat_456",
"agentIds": ["assistant"],
"messageId": "msg_789",
"senderId": "user",
"role": "user",
"content": "Hello, can you help me?",
"modelId": null,
"pendingRequestsCount": 0,
"trace": null
}request.resolved / request.cancelled
{
"chatId": "chat_456",
"requestId": "req_123",
"type": "confirmation",
"status": "resolved",
"resolutionPayload": { "confirmed": true },
"trace": null
}message.streaming.complete
{
"chatId": "chat_456",
"messageId": "msg_789",
"role": "assistant",
"streamState": "complete",
"finalText": "Here is my response."
}request.created
{
"chatId": "chat_456",
"requestId": "req_123",
"type": "confirmation",
"title": "Deploy to production?"
}media.attached
{
"chatId": "chat_456",
"mediaId": "media_789",
"messageId": "msg_123",
"kind": "image"
}chat.* events
Chat lifecycle events (chat.created, chat.updated, chat.archived, chat.unarchived, chat.read, chat.unread) contain:
{
"chatId": "chat_456"
}Signature verification
When a signingSecret is configured, Opengram includes an X-OpenGram-Signature header with every delivery. The signature is computed as:
sha256=<hex-encoded HMAC-SHA256 of the raw request body>To verify a webhook in your handler:
- Read the raw request body as bytes.
- Compute the HMAC-SHA256 of the body using your
signingSecretas the key. - Compare the hex-encoded result with the value after
sha256=in the header. - Use a constant-time comparison to prevent timing attacks.
Node.js example
import { createHmac, timingSafeEqual } from "crypto";
function verifySignature(body, secret, signatureHeader) {
const hmac = createHmac("sha256", secret);
hmac.update(body);
const expected = `sha256=${hmac.digest("hex")}`;
return (
expected.length === signatureHeader.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader))
);
}Retry logic
When a delivery fails, Opengram retries with exponential backoff up to the configured maxRetries:
- 4xx responses are treated as permanent failures and are not retried.
- 5xx responses and network errors are retried.
- The backoff delay increases exponentially between attempts.
After all retry attempts are exhausted, the delivery is marked as failed.
Delivery logging
Every webhook delivery attempt is logged in the webhook_deliveries table with the HTTP status code, success/failure status, and any error message. Delivery records are retained for 30 days alongside their parent events. This log is useful for debugging delivery issues.