ChainLaunch

Pro Feature

Webhooks

Outbound webhooks require ChainLaunch Pro. Learn more about Pro features.

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 "", 200

Node.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: maxRetries attempts (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 id field in the envelope as your idempotency key.
  • Ordering: not guaranteed across event types. For ordering-sensitive flows (e.g. node.stopped followed by node.started), your receiver should compare timestamp.

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-Instance header — 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 (use customHeaders to pass a client cert hash if your reverse proxy supports it).

Next steps