Webhooks
Webhooks deliver platform events to an HTTP endpoint you control, signed with HMAC-SHA256 so you can verify the payload came from Amdahl. Use them to react to artifact creation, session state changes, audit log entries, and every other event the platform emits.
For the complete event catalogue and per-event payload shapes see API reference: webhook events.
Overview
- Events fan out to every active webhook whose
eventsarray contains the event name. - Each delivery is a single
POSTwith a JSON body and three headers:Content-Type: application/json,X-Webhook-Signature,X-Webhook-Event. - Signatures are HMAC-SHA256 hex digests of the raw request body, keyed on a per-webhook secret returned once at registration time.
- Failed deliveries retry up to 3 times with exponential backoff, then give up and increment the webhook's
failure_count.
Register a webhook
POST /api/platform/v1/webhooks
curl -X POST https://api.amdahl.com/api/platform/v1/webhooks \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/amdahl/webhook",
"events": ["artifact.created", "artifact.updated", "audit_log.entry_created"],
"description": "Ingest Amdahl artifacts into our CMS"
}'Response:
{
"id": "whk_01hq2...",
"business_id": "biz_01hq...",
"user_id": "usr_01hq...",
"url": "https://your-app.example.com/amdahl/webhook",
"events": ["artifact.created", "artifact.updated", "audit_log.entry_created"],
"description": "Ingest Amdahl artifacts into our CMS",
"active": true,
"created_at": "2026-04-14T18:22:14.509Z",
"secret": "a1b2c3d4e5f6...64chars"
}The secret field is only returned once, at registration time. Store it somewhere you can retrieve it server-side when a delivery arrives; Amdahl stores it encrypted and cannot return the plaintext later. If you lose it, delete the webhook and register a new one.
You can supply your own secret via the optional secret field on the request body if you want to rotate from an existing deployment. When omitted the platform generates a 64-character hex string.
See API reference: webhook events for the full list of subscribable events. Common ones include:
artifact.created,artifact.updated,artifact.deletedsession.started,session.endedaudit_log.entry_createdagent.session.completed(roadmap)
Payload envelope
Every delivery has the same outer shape:
{
"event": "artifact.created",
"timestamp": "2026-04-14T18:23:02.462Z",
"business_id": "biz_01hq...",
"data": {
"artifact_id": "art_01hq...",
"artifact_type": "content_piece",
"title": "Q2 earnings highlights",
"created_by": "usr_01hq..."
}
}eventis the event name.timestampis when the event fired, ISO 8601 in UTC.business_idscopes the event to one tenant. Always validate that it matches the business you expect.datacarries per-event fields. See webhook events.
Signature verification
The signature is the HMAC-SHA256 hex digest of the raw request body, computed with your per-webhook secret. Always verify before trusting the payload, and always use a constant-time comparison so timing side channels do not leak the expected digest.
TypeScript / Node
import crypto from 'node:crypto'
import express from 'express'
const app = express()
app.post('/amdahl/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.header('X-Webhook-Signature') ?? ''
const secret = process.env.AMDAHL_WEBHOOK_SECRET!
const expected = crypto
.createHmac('sha256', secret)
.update(req.body) // Buffer from express.raw
.digest('hex')
const sigBuf = Buffer.from(signature, 'hex')
const expBuf = Buffer.from(expected, 'hex')
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
return res.status(401).send('invalid signature')
}
const event = JSON.parse(req.body.toString('utf8'))
// process event, then ACK fast
res.status(200).send('ok')
})Two things to watch:
- Use
express.raw()(notexpress.json()) on the webhook route so you verify against the exact bytes the platform signed. - Compare with
crypto.timingSafeEqual, not===.
Python
import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["AMDAHL_WEBHOOK_SECRET"].encode("utf-8")
@app.post("/amdahl/webhook")
def amdahl_webhook():
signature = request.headers.get("X-Webhook-Signature", "")
body = request.get_data() # raw bytes
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401, "invalid signature")
event = request.get_json(force=True)
# process event, then ACK fast
return ("ok", 200)Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
)
var secret = []byte(os.Getenv("AMDAHL_WEBHOOK_SECRET"))
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read error", http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, secret)
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
sig := r.Header.Get("X-Webhook-Signature")
if !hmac.Equal([]byte(sig), []byte(expected)) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
// parse body as JSON, process, then ACK fast
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}Retry policy
Failed deliveries retry automatically. The platform treats any response outside the 2xx range, plus network errors and 10-second timeouts, as failed.
| Attempt | Delay before retry |
|---|---|
| 1 | (initial delivery) |
| 2 | 1 second |
| 3 | 10 seconds |
After three failed attempts the platform stops retrying that event and increments the webhook's failure_count. Every attempt (success or failure) is recorded in the delivery log.
If your endpoint is persistently down, operators may temporarily disable the webhook. You can reactivate it with PATCH /api/platform/v1/webhooks/:id setting active: true once you are back.
Delivery history
Inspect the delivery log any time:
curl https://api.amdahl.com/api/platform/v1/webhooks/$WEBHOOK_ID/deliveries \
-H "Authorization: Bearer $API_KEY"Response:
{
"deliveries": [
{
"id": "whd_01hq...",
"event": "artifact.created",
"status_code": 200,
"attempt": 1,
"error": null,
"status": "success",
"duration_ms": 124,
"is_test": false,
"created_at": "2026-04-14T18:23:02.462Z"
},
{
"id": "whd_01hq...",
"event": "artifact.updated",
"status_code": 503,
"attempt": 3,
"error": "HTTP 503",
"status": "failed",
"duration_ms": 9800,
"is_test": false,
"created_at": "2026-04-14T18:10:18.118Z"
}
]
}Filter by status with ?status=failed and paginate with ?limit= and ?offset=. See Pagination and errors.
Test fire
Send a synthetic test.ping to the registered URL without affecting production aggregates:
curl -X POST https://api.amdahl.com/api/platform/v1/webhooks/$WEBHOOK_ID/test \
-H "Authorization: Bearer $API_KEY"Response contains the captured HTTP status, response body (up to 4KB), duration, and delivery log id:
{
"delivery_id": "whd_01hq...",
"status_code": 200,
"response_body": "ok",
"duration_ms": 93,
"success": true,
"error": null
}Test deliveries are persisted with is_test: true so you can filter them out of your dashboards. They do not retry.
Update, disable, delete
GET /api/platform/v1/webhookslists your webhooks (secrets are never included).PATCH /api/platform/v1/webhooks/:idupdatesurl,events,description, oractive. Secret rotation is not supported in-place; delete and re-register.DELETE /api/platform/v1/webhooks/:idsoft-deletes. Delivery history is retained for auditability.
Best practices
- ACK fast. Return
2xxwithin a second or two. Process the payload asynchronously on your side (enqueue, then respond). The 10-second timeout is a hard ceiling, not a target. - Deduplicate on event id. The
datablock for most events carries a stable id (artifact_id,session_id,entry_id). If your processing is not naturally idempotent, record the id to detect replays. - Verify before parsing. Do the signature check against the raw bytes, then parse JSON. Never the other way around.
- Store the secret in a secrets manager. Treat it like any other long-lived credential. Rotate by deleting the webhook and registering a new one.
- Monitor
failure_count. PullGET /webhooksperiodically in your ops tooling and alert when a webhook accumulates failures, so you catch outbound outages before they expire events. - Filter by
business_iddefensively. If one endpoint serves multiple tenants you ingested through Amdahl, always key work off the payloadbusiness_id, never a URL segment or cookie.
See also
- API reference: webhook events for event names and per-event payloads
- Recipe: react to a webhook for a full end-to-end worked example
- API reference: errors for the canonical error envelope
- Authentication for the API key scopes required to manage webhooks