Autumn has first-class support for outbound signed webhook delivery, allowing your application to dispatch structured event payloads to external API consumers safely and reliably.

Architectural Overview

The outbound webhook subsystem consists of five core components:

  1. WebhookSubscription: Represents a consumer's registered endpoint, signing secret, the event topics they are interested in, and their active status.
  2. OutboundWebhookStore: A pluggable trait for persisting subscriptions and tracking delivery attempts, with a process-local InMemoryOutboundWebhookStore provided by default.
  3. WebhookOutboundManager: The central coordinator available via AppState extensions, providing the .dispatch() method to transactionally log and enqueue events.
  4. autumn_webhook_delivery Job: A resilient background job that handles HTTP POST delivery, computes payload signatures, executes retries, and handles deactivations.
  5. Actuator Operations: Sensitive API endpoints under /actuator/webhooks/* for monitoring the Dead Letter Queue (DLQ) and replaying permanently failed deliveries.

1. Webhook Subscriptions

A subscription is registered for a set of event topics and points to a target destination URL:

Rust
use autumn_web::webhook_outbound::{WebhookSubscription, WebhookSubscriptionStatus};

let subscription = WebhookSubscription {
    id: "sub_123".to_owned(),
    target_url: "https://api.consumer.com/webhooks/receiver".to_owned(),
    event_topics: vec!["order.created".to_owned(), "order.fulfilled".to_owned()],
    secret: "whsec_stripe_style_signing_secret_key_32_bytes!!".to_owned(),
    status: WebhookSubscriptionStatus::Active,
    consecutive_failures: 0,
};

Subscription Statuses

  • Active: The subscription is operational; events will be dispatched immediately.
  • Disabled: The subscription is manually turned off by the operator or consumer.
  • Failed: The subscription has been automatically deactivated after exceeding the maximum failure threshold (50 consecutive failures) to protect your application resources and avoid thundering herd requests on failing external servers.

2. Pluggable Storage Backend

To support diverse hosting environments, the persistence layer is abstracted behind the OutboundWebhookStore trait. You can implement this trait to store subscription states and delivery logs in PostgreSQL, Redis, MongoDB, or any external service.

By default, Autumn provides InMemoryOutboundWebhookStore—a bounded, thread-safe, in-memory store ideal for tests, development, or lightweight deployments.


3. Stripe-Style Payload Signing

Security is established using Stripe-style HMAC-SHA256 payload signing. Every request body is signed using the subscription's secret key and sent via the Autumn-Signature header in the format:

Http
Autumn-Signature: t=1778930400,v1=a1b2c3d4e5f6...
  • t: The Unix epoch timestamp of the delivery dispatch.
  • v1: The computed HMAC-SHA256 hex signature of the string {timestamp}.{raw_body}.

Verification (Consumer side)

The consumer receives the header, extracts t and v1, concatenates t and the raw request body bytes with a ., computes the HMAC-SHA256 using their registered secret, and compares it securely with v1 to prevent timing attacks.


4. Retries, Jitter, and the Dead Letter Queue (DLQ)

If a webhook delivery fails (network exception, connection timeout, or a non-2xx HTTP status code), the background job initiates a robust retry flow:

  • Exponential Backoff: Retries are scheduled with progressive delays: $Delay = Base \times 2^{attempt-1} + Jitter$.
  • Jitter: A randomized offset between $0.0$ and $10.0$ seconds is injected to prevent thundering herd congestion on receiving servers.
  • Capped Attempts: Delivery is retried up to a maximum of 5 attempts.
  • Dead Letter Queue (DLQ): If all 5 attempts fail, the delivery log is permanently archived as is_dlq = true and retired from active background processing.

5. Actuator Observability and Replaying

Operators can monitor the state of outbound webhooks and manually trigger re-deliveries of DLQ logs using the sensitive actuator routes:

List DLQ Deliveries

Retrieve all permanently failed webhook delivery attempts:

Http
GET /actuator/webhooks/dlq

Response (JSON):

Json
[
  {
    "id": "log_456",
    "subscription_id": "sub_123",
    "topic": "order.created",
    "payload": "{\"order_id\":\"ord_999\"}",
    "request_headers": {
      "Content-Type": "application/json",
      "Autumn-Signature": "t=1778930400,v1=..."
    },
    "response_status": 500,
    "response_body": "{\"error\":\"Internal Server Error\"}",
    "elapsed_ms": 142,
    "attempt": 5,
    "max_attempts": 5,
    "is_dlq": true,
    "last_error": "server returned status: 500",
    "timestamp": "2026-05-26T05:00:00Z"
  }
]

Replay a DLQ Log

Manually reset and trigger re-delivery of a dead-lettered log. The system will reset the attempt counter back to 1, mark the log as no longer in the DLQ, and re-enqueue a fresh background delivery task:

Http
POST /actuator/webhooks/replay
Content-Type: application/json

{
  "log_id": "log_456"
}

6. AppBuilder Integration

To enable outbound webhooks, configure your store and register the OutboundWebhookPlugin in your application setup:

Rust
use std::sync::Arc;
use autumn_web::prelude::*;
use autumn_web::webhook_outbound::{InMemoryOutboundWebhookStore, OutboundWebhookPlugin};

#[autumn_web::main]
async fn main() {
    let store = Arc::new(InMemoryOutboundWebhookStore::new());
    let webhook_plugin = OutboundWebhookPlugin::new(store.clone())
        .with_initial_backoff_ms(1000); // 1s base retry backoff

    autumn_web::app()
        .plugin(webhook_plugin)
        .run()
        .await;
}

7. Dispatching Events

Within your HTTP handlers or workflow tasks, extract the WebhookOutboundManager from application extensions to dispatch structured payloads:

Rust
use autumn_web::prelude::*;
use autumn_web::webhook_outbound::WebhookOutboundManager;

#[post("/orders")]
async fn create_order(
    state: State<AppState>,
    Json(payload): Json<CreateOrderPayload>,
) -> AutumnResult<Json<Order>> {
    let order = save_order_to_db(&payload).await?;

    // Fetch the manager from app extensions
    let manager = state.extension::<WebhookOutboundManager>()
        .ok_or_else(|| AutumnError::internal_server_error_msg("Webhook outbound subsystem not registered"))?;

    // Dispatch transactionally to all matching active subscribers
    manager.dispatch(&state, "order.created", &order).await?;

    Ok(Json(order))
}