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.
| Mode | Use it for |
|---|---|
| Execution event readback | Reconciliation and historical inspection. |
| SSE | Live UI updates and short-lived monitoring sessions. |
| Webhook subscription | Durable server-to-server delivery with retries. |
curl --fail-with-body --silent --show-error \
"$ALFRED_API_URL/v1/executions/$EXECUTION_ID/events" \
-H "Authorization: Bearer $ALFRED_API_KEY" | jqEvents 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.
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:
eventexecutionIdprincipalIdworkflowSSE 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.
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.
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"
}' | jqWebhook URLs must use HTTPS and resolve to public addresses. Alfred rejects private, loopback, link-local, and internal destinations.
The request body fields are:
| Field | Purpose |
|---|---|
url | Public HTTPS callback URL. |
events | Non-empty list of exact event types, such as CertificateIssued.v2. |
active | Whether Alfred should materialize deliveries; defaults to true. |
secret | Shared 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.
Alfred sends:
X-ProfessorX-Event-Id: evt_...
X-ProfessorX-Event-Type: CertificateIssued.v2
X-ProfessorX-Signature: t=unix-seconds,v1=hex-hmacRead 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.
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" | jqQueue 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" | jqChange 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"]
}' | jqRotate 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"}' | jqThe 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.