Browse docs (11)

Outbound Webhooks Guide

821 words ยท 4 min read ยท 7 sections

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 Webhooks

What 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:
HeaderValue
Content-Typeapplication/json
X-Mgl-EventEvent type, e.g. incident.created
X-Mgl-Event-IdSame as id โ€” use for dedup
X-Mgl-TeamTeam UUID
X-Mgl-Signaturesha256=<hex> HMAC of raw body with your secret (absent if no secret configured)
X-Mgl-Delivery-Attempt1, 2, or 3 โ€” retries on 5xx / 429 / timeout

Event types

TypeEmitted todayData 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.executedPlanned (emits when agent autonomous runtime lands)fixId, sessionId, action, target, status, result, approvedBy, durationMs
fix.failedPlannedsame 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.deleted

Verifying 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 responseOur behavior
2xxSuccess, done
3xxFollows redirect (Next.js fetch default)
4xx (not 429)Terminal โ€” we mark failed, no retry. You rejected our format; fix your handler.
429Retry with exponential backoff
5xxRetry 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.