Overview
Push MicroGemLabs events to any HTTP endpoint: PagerDuty, Zapier, an n8n workflow, your own internal Slack bot, a custom dashboard โ anywhere you can receive a signed POST.
Configure at: microgemlabs.ai/integrations โ Outbound WebhooksWhat you get
Each webhook delivery is a JSON envelope with a stable shape:
{
"id": "evt_a1b2c3d4-โฆ",
"type": "incident.created",
"teamId": "team_โฆ",
"timestamp": "2026-04-24T02:18:00.000Z",
"version": "v1",
"data": { /* type-specific payload */ }
}
Headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Mgl-Event | Event type, e.g. incident.created |
X-Mgl-Event-Id | Same as id โ use for dedup |
X-Mgl-Team | Team UUID |
X-Mgl-Signature | sha256=<hex> HMAC of raw body with your secret (absent if no secret configured) |
X-Mgl-Delivery-Attempt | 1, 2, or 3 โ retries on 5xx / 429 / timeout |
Event types
| Type | Emitted today | Data shape |
|---|---|---|
incident.created | โ | incidentId, product, resourceId, resourceName, severity, startedAt |
incident.resolved | โ | + endedAt, durationSeconds |
incident.acknowledged | โ
(via mgl incident ack / API) | + acknowledgedBy |
proposal.raised | โ | notificationId, sessionId, action, target, rationale, severity, expiresAt |
proposal.approved | โ | + respondedBy, respondedAt |
proposal.denied | โ | + respondedBy, respondedAt |
fix.executed | Planned (emits when agent autonomous runtime lands) | fixId, sessionId, action, target, status, result, approvedBy, durationMs |
fix.failed | Planned | same as fix.executed |
runbook.executed | โ | executionId, runbookId, runbookName, triggeredBy, actor, status, httpStatus, durationMs |
anomaly.detected | โ | metric, resourceId, resourceName, currentValue, baselineMean, zScore, sensitivityThreshold |
audit.logged | โ | auditId, action, actorType, actorUserId, actorLabel, targetType, targetId, metadata |
The "planned" rows are scaffolded in the event taxonomy but the source products haven't wired them up yet. They'll light up transparently โ your receiver doesn't need to change.
Audit stream
Every sensitive team operation (credential add/remove, tier changes, proposal approvals, runbook executions, webhook CRUD, team membership changes) also fires an audit.logged event carrying the same row that appears on the /team/audit page. If you pipe this into your SIEM you have a complete who-did-what feed without scraping the dashboard.
Canonical action values currently emitted:
team.member.removed team.member.role_changed
team.invite.revoked team.member.left
agent.tier.changed agent.subscription.cancelled
agent.credential.added agent.credential.removed
agent.config.updated proposal.approved
proposal.denied runbook.executed
webhook.created webhook.updated
webhook.deletedVerifying signatures
Compute HMAC-SHA256 of the raw request body (bytes, not re-parsed JSON) using the signing secret from the webhook config page. Compare timing-safe against X-Mgl-Signature.
Node.js
import crypto from 'node:crypto'
import express from 'express'
const app = express()
const SECRET = process.env.MGL_WEBHOOK_SECRET
// Capture raw body; express.json() would re-serialize and break HMAC
app.use(express.raw({ type: 'application/json' }))
app.post('/mgl-webhook', (req, res) => {
const signatureHeader = req.header('x-mgl-signature') || ''
const expected = 'sha256=' +
crypto.createHmac('sha256', SECRET).update(req.body).digest('hex')
const a = Buffer.from(signatureHeader, 'utf8')
const b = Buffer.from(expected, 'utf8')
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('bad signature')
}
const event = JSON.parse(req.body.toString('utf8'))
console.log(event.type, event.data)
res.status(200).send('ok')
})
app.listen(3000)
Python
import hmac, hashlib
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ['MGL_WEBHOOK_SECRET']
@app.post('/mgl-webhook')
def webhook():
raw = request.get_data()
expected = 'sha256=' + hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get('X-Mgl-Signature', '')):
abort(401)
event = request.get_json()
print(event['type'], event['data'])
return 'ok', 200
Dedup
Stripe-style: we retry on 5xx / 429 / timeout up to 3 times with exponential backoff. Same X-Mgl-Event-Id on each attempt โ dedup on that if your side-effects are sensitive.
Retry semantics
| Receiver response | Our behavior |
|---|---|
| 2xx | Success, done |
| 3xx | Follows redirect (Next.js fetch default) |
| 4xx (not 429) | Terminal โ we mark failed, no retry. You rejected our format; fix your handler. |
| 429 | Retry with exponential backoff |
| 5xx | Retry with exponential backoff |
| Timeout (>10s) | Retry with exponential backoff |
Max 3 attempts total. After that the delivery is logged as failed and dropped โ we don't queue indefinitely.
Testing
The webhook manager has a Test button per row. It fires a synthetic incident.created event with data.test: true through the normal delivery path (signed, retried, same headers). Use it to sanity-check your receiver before waiting for a real incident.
Security notes
- SSRF guard โ URLs that resolve to private IP ranges, cloud metadata endpoints,
file://,localhost,.internal, etc. are rejected at create time and at delivery time. - Signatures are optional but recommended. Unsigned webhooks still work (legacy alert-channel webhooks may not have a secret); add one to existing webhooks via the Edit flow.
- Secret rotation โ generate a new secret in the Edit form. Old deliveries-in-flight use whatever secret was in config when the Inngest function picked them up; the next delivery uses the new value. There's no "overlap" period, so deploy the new secret to your receiver first.
- What we log โ URL, event type, delivery attempt, HTTP status. Never the request/response body or signature, so leaked logs can't be replayed.