Docs

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 events array contains the event name.
  • Each delivery is a single POST with 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

bash
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:

json
{
  "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.deleted
  • session.started, session.ended
  • audit_log.entry_created
  • agent.session.completed (roadmap)

Payload envelope

Every delivery has the same outer shape:

json
{
  "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..."
  }
}
  • event is the event name.
  • timestamp is when the event fired, ISO 8601 in UTC.
  • business_id scopes the event to one tenant. Always validate that it matches the business you expect.
  • data carries 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

typescript
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:

  1. Use express.raw() (not express.json()) on the webhook route so you verify against the exact bytes the platform signed.
  2. Compare with crypto.timingSafeEqual, not ===.

Python

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

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.

AttemptDelay before retry
1(initial delivery)
21 second
310 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:

bash
curl https://api.amdahl.com/api/platform/v1/webhooks/$WEBHOOK_ID/deliveries \
  -H "Authorization: Bearer $API_KEY"

Response:

json
{
  "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:

bash
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:

json
{
  "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/webhooks lists your webhooks (secrets are never included).
  • PATCH /api/platform/v1/webhooks/:id updates url, events, description, or active. Secret rotation is not supported in-place; delete and re-register.
  • DELETE /api/platform/v1/webhooks/:id soft-deletes. Delivery history is retained for auditability.

Best practices

  • ACK fast. Return 2xx within 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 data block 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. Pull GET /webhooks periodically in your ops tooling and alert when a webhook accumulates failures, so you catch outbound outages before they expire events.
  • Filter by business_id defensively. If one endpoint serves multiple tenants you ingested through Amdahl, always key work off the payload business_id, never a URL segment or cookie.

See also