Consume Events

Read execution history, connect to SSE, or subscribe to signed webhooks.

Alfred exposes the same versioned outbox events through readback, live SSE, and durable webhook delivery.

Use an API key with the workflows:<name> permission for each workflow whose events you need. Use subscriptions:manage for webhook subscription operations.

Choose a Delivery Mode

ModeUse it for
Execution event readbackReconciliation and historical inspection.
SSELive UI updates and short-lived monitoring sessions.
Webhook subscriptionDurable server-to-server delivery with retries.

Read Historical Events

curl --fail-with-body --silent --show-error \
  "$ALFRED_API_URL/v1/executions/$EXECUTION_ID/events" \
  -H "Authorization: Bearer $ALFRED_API_KEY" | jq

Events are ordered by occurrence time and event ID.

Add Accept: text/event-stream to this same execution route when you want durable replay followed by live updates for only that execution.

Connect to the Global SSE Stream

curl -N --fail-with-body \
  "$ALFRED_API_URL/v1/streams/events?executionId=$EXECUTION_ID" \
  -H "Authorization: Bearer $ALFRED_API_KEY"

Supported query keys are repeatable or comma-separated:

  • event
  • executionId
  • principalId
  • workflow

SSE visibility follows the principal's current workflow permissions. Alfred rechecks them when a stream is opened and includes only events for allowed workflows. A workflow or execution filter can narrow this set but cannot expand it.

Use Last-Event-ID when reconnecting. Handle the stream-overflow and server-shutdown control events by reconnecting with the last processed event ID.

Use SSE from a Browser

Mint a single-use stream token:

token=$(
  curl --fail-with-body --silent --show-error \
    -X POST "$ALFRED_API_URL/v1/streams/token" \
    -H "Authorization: Bearer $ALFRED_API_KEY" |
    jq -r '.token'
)

Then connect without putting the API key in the URL:

const stream = new EventSource(
  `${alfredApiUrl}/v1/streams/events?executionId=${executionId}&token=${token}`,
);

Stream tokens expire after 300 seconds and are consumed on first use. Mint a new token for every reconnection attempt.

Create a Webhook Subscription

First deploy a public HTTPS callback in the consumer service. It must accept Alfred's JSON request body and return an HTTP response. The hostname must resolve publicly: Alfred rejects private, loopback, link-local, and internal destinations when the subscription is created and before each delivery.

Give the registering principal an API key with subscriptions:manage, then call POST /v1/subscriptions. No consumer repository configuration is required: the consumer registers its callback through Alfred's API. Add repository configuration only when the consumer itself will automate this registration call.

Choose a secret known to both systems and register the exact active event types the consumer handles:

curl --fail-with-body --silent --show-error \
  -X POST "$ALFRED_API_URL/v1/subscriptions" \
  -H "Authorization: Bearer $ALFRED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "active": true,
    "events": ["CertificateIssued.v2", "CertificateFailed.v1"],
    "secret": "replace-with-a-long-random-secret",
    "url": "https://consumer.example.com/alfred/events"
  }' | jq

Webhook URLs must use HTTPS and resolve to public addresses. Alfred rejects private, loopback, link-local, and internal destinations.

The request body fields are:

FieldPurpose
urlPublic HTTPS callback URL.
eventsNon-empty list of exact event types, such as CertificateIssued.v2.
activeWhether Alfred should materialize deliveries; defaults to true.
secretShared secret used to sign requests. Alfred encrypts it and never returns it.

The create operation returns HTTP 201 with the subscription record. Save its id for lifecycle operations.

Verify a Webhook

Alfred sends:

X-ProfessorX-Event-Id: evt_...
X-ProfessorX-Event-Type: CertificateIssued.v2
X-ProfessorX-Signature: t=unix-seconds,v1=hex-hmac

Read the request as bytes before parsing JSON. Compute HMAC-SHA256 over the timestamp and the exact raw request body:

<timestamp>.<raw-request-body>

Compare the hexadecimal digest with v1 using a constant-time comparison. Reject stale timestamps according to the consumer's replay window, then deduplicate the event ID from X-ProfessorX-Event-Id before applying side effects. Any HTTP 2xx response acknowledges the delivery. Other responses and request failures enter Alfred's retry handling.

Operate a Subscription

Inspect recent attempts with GET /v1/subscriptions/{id}/deliveries:

curl --fail-with-body --silent --show-error \
  "$ALFRED_API_URL/v1/subscriptions/$SUBSCRIPTION_ID/deliveries" \
  -H "Authorization: Bearer $ALFRED_API_KEY" | jq

Queue an existing event again with POST /v1/subscriptions/{id}/redeliver/{eventId}:

curl --fail-with-body --silent --show-error \
  -X POST \
  "$ALFRED_API_URL/v1/subscriptions/$SUBSCRIPTION_ID/redeliver/$EVENT_ID" \
  -H "Authorization: Bearer $ALFRED_API_KEY" | jq

Change the event selection or active state with PATCH /v1/subscriptions/{id}:

curl --fail-with-body --silent --show-error \
  -X PATCH "$ALFRED_API_URL/v1/subscriptions/$SUBSCRIPTION_ID" \
  -H "Authorization: Bearer $ALFRED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "active": true,
    "events": ["CertificateIssued.v2"]
  }' | jq

Rotate the signing secret with POST /v1/subscriptions/{id}/secret by providing the replacement:

curl --fail-with-body --silent --show-error \
  -X POST "$ALFRED_API_URL/v1/subscriptions/$SUBSCRIPTION_ID/secret" \
  -H "Authorization: Bearer $ALFRED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"secret":"replace-with-another-long-random-secret"}' | jq

The subscription response never returns plaintext secret material. Coordinate the replacement with the consumer before rotating it.

Disable future deliveries with DELETE /v1/subscriptions/{id}. This sets the subscription inactive and preserves its history:

curl --fail-with-body --silent --show-error \
  -X DELETE "$ALFRED_API_URL/v1/subscriptions/$SUBSCRIPTION_ID" \
  -H "Authorization: Bearer $ALFRED_API_KEY"

A successful disable returns HTTP 204 with no response body.