ChainLaunch Pro Feature
Outbound webhooks require ChainLaunch Pro. Learn more about Pro features.
ChainLaunch Pro pushes events to your systems via signed HTTP POST callbacks. Use webhooks to wire ChainLaunch into Slack, PagerDuty, Datadog, your own SOAR pipeline, or any internal service that needs to react to blockchain operations in real time.
Concepts
- Webhook — a destination URL plus configuration. You can register many.
- Event type — what triggered the webhook (
node.downtime,backup.failure, etc.). - Signature — every payload is signed with HMAC-SHA256 of your secret so receivers can verify authenticity.
- Subscription — each webhook subscribes to one or more event types. Events you don't subscribe to are not delivered.
Webhooks complement Notifications (email/Slack via templates). Notifications are for humans. Webhooks are for machines.
Quickstart
# Register a webhook
curl -X POST https://chainlaunch.example.com/api/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "PagerDuty critical",
"url": "https://events.pagerduty.com/integration/abcd/enqueue",
"secret": "whsec_8f3a...",
"events": [
"node.downtime",
"certificate.expired",
"backup.failure",
"system.disk_space.warning"
],
"customHeaders": { "X-Source": "chainlaunch-prod" },
"timeoutSeconds": 10,
"maxRetries": 3
}'ChainLaunch starts delivering events matching the subscription immediately. Test it:
curl -X POST https://chainlaunch.example.com/api/v1/webhooks/{id}/test \
-H "Authorization: Bearer $TOKEN"This sends a synthetic event of each subscribed type and reports the receiver's HTTP response.
Available events
There are 50+ event types organized in 10 categories. The full list is also available at GET /api/v1/webhooks/events.
System
| Event | Default severity | Triggered when |
|---|---|---|
system.disk_space.warning |
warning | Disk usage on the ChainLaunch host crosses the configured threshold |
system.s3.connection_issue |
critical | An S3-backed backup target fails connectivity check |
Backup
| Event | Severity | When |
|---|---|---|
backup.success |
success | A scheduled or on-demand backup completes |
backup.failure |
critical | A backup job errors out |
backup.scheduled |
info | A backup is queued |
Node lifecycle
| Event | Severity | When |
|---|---|---|
node.created |
info | A new node is registered |
node.started |
success | A node transitions to RUNNING |
node.stopped |
warning | A node is intentionally stopped |
node.deleted |
info | A node row is removed |
node.downtime |
critical | A running node fails its heartbeat |
node.recovery |
success | A previously-downed node recovers |
node.config_changed |
info | Node configuration is edited |
Blockchain
| Event | Severity | When |
|---|---|---|
blockchain.block.received |
info | A new block is observed (high volume — subscribe carefully) |
blockchain.transaction.committed |
info | A transaction is committed (also high volume) |
blockchain.chaincode.deployed |
success | Chaincode commit succeeds |
blockchain.chaincode.upgraded |
info | Chaincode upgraded |
blockchain.chaincode.invoked |
info | A chaincode invocation completes |
Network
| Event | Severity | When |
|---|---|---|
network.created |
info | A new network is created |
network.updated |
info | Network config changes |
network.deleted |
warning | A network row is removed |
network.channel.created |
info | A channel is created on a network |
network.organization.joined |
info | A new organization joins a channel |
network.anchor_peer.updated |
info | Anchor peer set is changed |
Certificate
| Event | Severity | When |
|---|---|---|
certificate.expiring |
warning | Cert expires within 30 days |
certificate.expired |
critical | Cert past its NotAfter |
certificate.renewed |
success | Manual renewal completed |
certificate.auto_renewed |
success | Automatic renewal completed |
certificate.renewal_failed |
critical | Renewal attempt errored |
Security
| Event | Severity | When |
|---|---|---|
security.unauthorized_access |
critical | Failed auth attempt with brute-force signature |
security.api_key.created |
info | API key minted |
security.api_key.revoked |
warning | API key revoked |
security.user.role_changed |
info | RBAC role assignment changed |
P2P sharing
| Event | Severity | When |
|---|---|---|
sharing.proposal.received / .shared |
info | Config proposal accepted/sent |
sharing.signature.received / .shared |
info | Endorsement signature exchanged |
sharing.network.received / .shared |
info | Network share lifecycle |
sharing.chaincode.received / .shared |
info | Chaincode share lifecycle |
Performance
| Event | Severity | When |
|---|---|---|
performance.cpu.high |
warning | CPU sustained above threshold |
performance.memory.high |
warning | Memory sustained above threshold |
performance.query.slow |
warning | A query exceeds the slow-query budget |
Plugin
| Event | Severity | When |
|---|---|---|
plugin.installed / .enabled |
info | Plugin lifecycle |
plugin.disabled |
warning | Plugin disabled |
Payload format
Every event payload follows this envelope:
{
"id": "wh_evt_01HW9...",
"event": "node.downtime",
"category": "Node",
"severity": "critical",
"timestamp": "2026-05-06T08:14:23.421Z",
"instance": {
"id": "chainlaunch-prod-eu",
"version": "1.x.y"
},
"data": {
"nodeId": 42,
"nodeName": "peer0-org1",
"nodeType": "FABRIC_PEER",
"lastHealthyAt": "2026-05-06T08:09:11Z",
"consecutiveFailures": 5
}
}The data object is event-specific. Field shapes are stable within a major version; new fields may be added in minor versions.
Verifying signatures
Every request carries two headers:
X-Webhook-Signature: 7b2f6e1c...
X-Webhook-Signature-256: sha256=7b2f6e1c...
Both are HMAC-SHA256 of the raw request body keyed with your registered secret. The hex form is in X-Webhook-Signature; the sha256= prefixed form (for receivers that follow the GitHub convention) is in X-Webhook-Signature-256.
Python receiver
import hmac, hashlib
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b"whsec_8f3a..."
@app.post("/chainlaunch/webhook")
def webhook():
sig = request.headers.get("X-Webhook-Signature", "")
expected = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
event = request.get_json()
print(event["event"], event["data"])
return "", 200Node.js receiver
import crypto from "crypto"
import express from "express"
const app = express()
const SECRET = process.env.CHAINLAUNCH_WEBHOOK_SECRET
app.post("/chainlaunch/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.headers["x-webhook-signature"]
const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex")
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).end()
}
const event = JSON.parse(req.body.toString())
console.log(event.event, event.data)
res.status(200).end()
})Go receiver
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func webhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(r.Header.Get("X-Webhook-Signature")), []byte(expected)) {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
// process event
w.WriteHeader(http.StatusOK)
}Always use a constant-time comparison (hmac.compare_digest, crypto.timingSafeEqual, hmac.Equal) — naive == leaks timing information.
Retries and delivery semantics
- Timeout: 10 seconds default, configurable per webhook (1–60s).
- Retries:
maxRetriesattempts (default 3) with exponential backoff (1s, 2s, 4s, ...). - Success criterion: HTTP 2xx response within the timeout. Anything else is a retryable failure.
- At-least-once delivery: a slow receiver might see a duplicate. Use the
idfield in the envelope as your idempotency key. - Ordering: not guaranteed across event types. For ordering-sensitive flows (e.g.
node.stoppedfollowed bynode.started), your receiver should comparetimestamp.
After all retries fail, ChainLaunch records the failure under Settings → Webhooks → (your webhook) → Recent Deliveries. You can replay manually from there or via:
curl -X POST https://chainlaunch.example.com/api/v1/webhooks/{id}/replay/{deliveryId}Filtering events
Subscribe with explicit event types — wildcards are not supported. Best practice is one webhook per receiver/role:
{
"name": "ops-pagerduty",
"events": [
"node.downtime", "node.recovery",
"certificate.expired", "certificate.renewal_failed",
"backup.failure",
"system.disk_space.warning", "system.s3.connection_issue"
]
}{
"name": "security-siem",
"events": [
"security.unauthorized_access",
"security.api_key.created", "security.api_key.revoked",
"security.user.role_changed"
]
}For very chatty channels (blockchain.block.received, blockchain.transaction.committed), make sure your receiver can handle the load — at busy networks this can be hundreds of events per second per channel.
Endpoint reference
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/webhooks |
List webhooks |
| POST | /api/v1/webhooks |
Create webhook |
| GET | /api/v1/webhooks/{id} |
Get one |
| PUT | /api/v1/webhooks/{id} |
Update |
| DELETE | /api/v1/webhooks/{id} |
Delete |
| POST | /api/v1/webhooks/{id}/test |
Send a synthetic test payload |
| GET | /api/v1/webhooks/{id}/deliveries |
Recent delivery attempts and outcomes |
| POST | /api/v1/webhooks/{id}/replay/{deliveryId} |
Re-send a failed delivery |
| GET | /api/v1/webhooks/events |
Catalog of available event types |
All require system:configure permission (ADMIN by default). See RBAC.
Common recipes
Slack alert on critical events
Slack incoming webhooks accept any JSON with a text field — but ChainLaunch sends our own envelope. Use a tiny relay:
// vercel/cloudflare worker
addEventListener("fetch", e => e.respondWith(handle(e.request)))
async function handle(req) {
const event = await req.json()
const slack = `https://hooks.slack.com/services/...`
await fetch(slack, {
method: "POST", headers: {"Content-Type":"application/json"},
body: JSON.stringify({
text: `:rotating_light: *${event.event}* (${event.severity})\n\`\`\`${JSON.stringify(event.data, null, 2)}\`\`\``
})
})
return new Response("", { status: 200 })
}PagerDuty Events API v2
Skip the relay — point the webhook directly at https://events.pagerduty.com/v2/enqueue and use Events API v2's routing_key mechanism. ChainLaunch's severity field maps cleanly to PagerDuty severities.
Audit pipeline (SIEM)
Subscribe a webhook to all security.* events and forward to your SIEM. Pair with the Audit Log export for the complete trail.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Receiver gets requests but signature check fails | Body is being parsed by middleware before signature verification | Verify against the raw body (use express.raw, request.get_data(), io.ReadAll) |
| All events timing out | Receiver is slower than 10s | Increase timeoutSeconds; or have receiver enqueue and return 202 quickly |
| Some events not delivered | Receiver returned non-2xx | Check Recent Deliveries for response body; retries exhaust after maxRetries |
| Duplicate events in receiver | At-least-once retry semantics | Idempotently process by envelope id |
| Webhook stuck "disabled" | ChainLaunch auto-disables after n consecutive failures |
Re-enable via UI after fixing the receiver — failures continue counting until first success |
| Test endpoint returns 200 but real events don't fire | Subscription doesn't include the actual event type | Check Events under the webhook config; use GET /webhooks/events for the canonical list |
Security checklist
- Use a strong, random secret per webhook (
openssl rand -hex 32). Store in your receiver's secret manager. - Always verify the signature. Never trust the
X-ChainLaunch-Instanceheader — it's spoofable. - Receivers should respond fast (under 1s) and do real work async. Otherwise retries pile up.
- Use HTTPS endpoints only. Plain HTTP works but exposes the signed payload to any on-path observer.
- Rotate secrets at least annually, immediately on suspected leak. Update on receiver before updating in ChainLaunch — there's a brief window where both must accept either secret.
- For very sensitive events (
security.*), double-protect with mTLS (usecustomHeadersto pass a client cert hash if your reverse proxy supports it).
Next steps
- Notifications — email/Slack templated notifications for humans.
- Audit Logging — review the events your webhooks fire on.
- Compliance Scanning — pair
compliance.scan.completedwith a webhook for daily reports. - Configure Monitoring — Prometheus metrics for the webhook delivery system itself.