Webhooks Module

Webhook orchestration for sending and receiving HTTP callbacks in the Armature framework.

Table of Contents

Overview

The armature-webhooks module provides comprehensive webhook support including:

  • Outgoing Webhooks: Send HTTP callbacks to registered endpoints
  • Incoming Webhooks: Receive and verify webhooks from external services
  • Signature Verification: HMAC-SHA256 signing and verification
  • Automatic Retries: Configurable retry policies with exponential backoff
  • Event System: Subscribe endpoints to specific event types

Features

  • โœ… Send webhooks with automatic HMAC-SHA256 signatures
  • โœ… Receive and verify incoming webhooks
  • โœ… Event-based endpoint subscriptions with wildcards
  • โœ… Configurable retry policies (fixed, exponential backoff)
  • โœ… Delivery tracking and status monitoring
  • โœ… Endpoint registry with failure tracking
  • โœ… Timestamp validation to prevent replay attacks
  • โœ… Custom headers per endpoint

Installation

Add the webhooks feature to your Cargo.toml:

[dependencies]
armature-framework = { version = "0.1", features = ["webhooks"] }

Or use the crate directly:

[dependencies]
armature-webhooks = "0.1"

Sending Webhooks

Basic Usage

use armature_webhooks::{WebhookClient, WebhookConfig, WebhookPayload};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = WebhookClient::new(WebhookConfig::default());

    let payload = WebhookPayload::new("user.created")
        .with_data(serde_json::json!({
            "user_id": "usr_123",
            "email": "user@example.com"
        }));

    // Send to a specific URL
    let delivery = client.send("https://example.com/webhook", payload).await?;

    println!("Delivery status: {:?}", delivery.status);
    Ok(())
}

With Signing Secret

let delivery = client
    .send_with_secret(
        "https://example.com/webhook",
        payload,
        Some("your-signing-secret"),
    )
    .await?;

Dispatching to Multiple Endpoints

use std::sync::Arc;

let registry = Arc::new(WebhookRegistry::new());

// Register endpoints
registry.register(
    WebhookEndpoint::builder("https://api1.example.com/webhook")
        .events(vec!["user.*"])
        .build()
);

registry.register(
    WebhookEndpoint::builder("https://api2.example.com/webhook")
        .events(vec!["user.created", "order.*"])
        .build()
);

let client = WebhookClient::with_registry(WebhookConfig::default(), registry);

// Dispatch to all subscribed endpoints
let payload = WebhookPayload::new("user.created")
    .with_data(serde_json::json!({"user_id": "123"}));

let deliveries = client.dispatch(payload).await?;
println!("Sent to {} endpoints", deliveries.len());

Receiving Webhooks

Verify and Parse

use armature_webhooks::WebhookReceiver;

let receiver = WebhookReceiver::new("your-signing-secret");

// In your HTTP handler
fn handle_webhook(body: &[u8], signature_header: &str) -> Result<(), WebhookError> {
    // Verify and parse in one step
    let payload = receiver.receive(body, signature_header)?;

    println!("Received event: {}", payload.event);
    println!("Data: {:?}", payload.data);

    Ok(())
}

Verify from HTTP Headers

use std::collections::HashMap;

let mut headers = HashMap::new();
headers.insert("X-Webhook-Signature".to_string(), signature.to_string());

let is_valid = receiver.verify_from_headers(&body, &headers)?;

Event Handlers

let handler = receiver.handler("user.*", |payload| {
    println!("Received user event: {}", payload.event);
    // Process the webhook
    Ok(())
});

// Handle incoming request
let handled = handler.handle(&body, &signature)?;

Endpoint Registry

Creating and Managing Endpoints

use armature_webhooks::{WebhookRegistry, WebhookEndpoint};

let registry = WebhookRegistry::new();

// Create an endpoint with builder
let endpoint = WebhookEndpoint::builder("https://api.example.com/webhooks")
    .events(vec!["user.created", "user.updated", "order.*"])
    .description("Main API webhook")
    .header("X-Custom-Header", "custom-value")
    .build();

let endpoint_id = registry.register(endpoint);

// Query endpoints
let user_endpoints = registry.get_endpoints_for_event("user.created");
let all_endpoints = registry.get_all();

// Manage endpoints
registry.enable(&endpoint_id)?;
registry.disable(&endpoint_id)?;
registry.unregister(&endpoint_id);

Event Wildcards

// Subscribe to all user events
let endpoint = WebhookEndpoint::builder("https://example.com")
    .events(vec!["user.*"])
    .build();

// Subscribe to all events
let endpoint = WebhookEndpoint::builder("https://example.com")
    .all_events()
    .build();

// Subscribe to specific events only
let endpoint = WebhookEndpoint::builder("https://example.com")
    .events(vec!["user.created", "order.completed"])
    .build();

Failure Tracking

// Record delivery results
registry.record_success(&endpoint_id)?;
registry.record_failure(&endpoint_id)?;

// Find problematic endpoints
let failing = registry.get_failing_endpoints(5); // โ‰ฅ5 consecutive failures

// Auto-disable failing endpoints
if endpoint.failure_count >= 10 {
    registry.disable(&endpoint.id)?;
}

Signature Security

How Signatures Work

Webhooks are signed using HMAC-SHA256:

  1. A timestamp is included to prevent replay attacks
  2. The signature covers both timestamp and payload
  3. Format: t=1234567890,v1=<hex-encoded-signature>

Signature Format

X-Webhook-Signature: t=1234567890,v1=abc123def456...

Where:

  • t = Unix timestamp when signature was created
  • v1 = HMAC-SHA256 signature (hex encoded)

Timestamp Validation

// Default: 5 minutes tolerance
let receiver = WebhookReceiver::new("secret");

// Custom tolerance
let receiver = WebhookReceiver::new("secret")
    .with_tolerance(60); // 60 seconds

Secret Rotation

let mut endpoint = registry.get(&endpoint_id).unwrap();

// Rotate to a new secret
let new_secret = endpoint.rotate_secret();
println!("New secret: {}", new_secret);

// Update in registry
registry.update(&endpoint_id, endpoint)?;

Retry Policies

Available Policies

use armature_webhooks::RetryPolicy;
use std::time::Duration;

// No retries
let policy = RetryPolicy::none();

// Fixed delay between retries
let policy = RetryPolicy::fixed(3, Duration::from_secs(5));
// Retries: 5s, 5s, 5s

// Exponential backoff (default)
let policy = RetryPolicy::exponential(5);
// Retries: 1s, 2s, 4s, 8s, 16s (capped at max_delay)

// Custom configuration
let policy = RetryPolicy {
    max_attempts: 5,
    initial_delay: Duration::from_secs(2),
    max_delay: Duration::from_secs(120),
    backoff_multiplier: 3.0,
    jitter: true,
};

Configuring the Client

let config = WebhookConfig::builder()
    .retry_policy(RetryPolicy::exponential(5))
    .timeout_secs(30)
    .build();

let client = WebhookClient::new(config);

Retryable Status Codes

The client automatically retries on these HTTP status codes:

  • 408 - Request Timeout
  • 429 - Too Many Requests
  • 500 - Internal Server Error
  • 502 - Bad Gateway
  • 503 - Service Unavailable
  • 504 - Gateway Timeout

Best Practices

1. Always Verify Signatures

// โŒ Bad - no verification
let payload: WebhookPayload = serde_json::from_slice(&body)?;

// โœ… Good - verify first
let payload = receiver.receive(&body, &signature)?;

2. Use Idempotency

// Store the webhook ID to prevent duplicate processing
let payload = receiver.receive(&body, &signature)?;

if already_processed(&payload.id) {
    return Ok(()); // Skip duplicate
}

process_webhook(&payload)?;
mark_processed(&payload.id);

3. Return Quickly

// โŒ Bad - slow processing blocks response
let payload = receiver.receive(&body, &signature)?;
heavy_processing(&payload)?; // Takes 30 seconds

// โœ… Good - queue for async processing
let payload = receiver.receive(&body, &signature)?;
queue.enqueue(payload)?;
Ok(()) // Return 200 immediately

4. Handle Failures Gracefully

let delivery = client.send(url, payload).await?;

match delivery.status {
    WebhookDeliveryStatus::Succeeded => {
        log::info!("Webhook delivered: {}", delivery.id);
    }
    WebhookDeliveryStatus::PermanentlyFailed => {
        log::error!("Webhook failed after {} attempts", delivery.attempts);
        // Alert, disable endpoint, etc.
    }
    _ => {
        // Pending or in-progress
    }
}

Common Pitfalls

  • โŒ Don't trust unverified webhooks
  • โŒ Don't process webhooks synchronously if they take time
  • โŒ Don't ignore timestamp validation (replay attacks)
  • โœ… Do implement idempotency
  • โœ… Do return 200 quickly and process asynchronously
  • โœ… Do monitor and alert on failing endpoints

API Reference

WebhookClient

impl WebhookClient {
    fn new(config: WebhookConfig) -> Self;
    fn with_registry(config: WebhookConfig, registry: Arc<WebhookRegistry>) -> Self;
    async fn send(&self, url: &str, payload: WebhookPayload) -> Result<WebhookDelivery>;
    async fn send_with_secret(&self, url: &str, payload: WebhookPayload, secret: Option<&str>) -> Result<WebhookDelivery>;
    async fn send_to_endpoint(&self, endpoint: &WebhookEndpoint, payload: WebhookPayload) -> Result<WebhookDelivery>;
    async fn dispatch(&self, payload: WebhookPayload) -> Result<Vec<WebhookDelivery>>;
}

WebhookReceiver

impl WebhookReceiver {
    fn new(secret: impl Into<String>) -> Self;
    fn with_tolerance(self, seconds: u64) -> Self;
    fn verify(&self, payload: &[u8], signature: &str) -> Result<bool>;
    fn receive(&self, payload: &[u8], signature: &str) -> Result<WebhookPayload>;
}

WebhookPayload

impl WebhookPayload {
    fn new(event: impl Into<String>) -> Self;
    fn with_data(self, data: serde_json::Value) -> Self;
    fn with_metadata(self, metadata: serde_json::Value) -> Self;
    fn to_bytes(&self) -> Result<Vec<u8>>;
    fn to_json(&self) -> Result<String>;
}

WebhookEndpoint

impl WebhookEndpoint {
    fn new(url: impl Into<String>) -> Self;
    fn builder(url: impl Into<String>) -> WebhookEndpointBuilder;
    fn is_subscribed_to(&self, event: &str) -> bool;
    fn rotate_secret(&mut self) -> String;
}

WebhookRegistry

impl WebhookRegistry {
    fn new() -> Self;
    fn register(&self, endpoint: WebhookEndpoint) -> String;
    fn unregister(&self, id: &str) -> Option<WebhookEndpoint>;
    fn get(&self, id: &str) -> Option<WebhookEndpoint>;
    fn get_endpoints_for_event(&self, event: &str) -> Vec<WebhookEndpoint>;
    fn enable(&self, id: &str) -> Result<()>;
    fn disable(&self, id: &str) -> Result<()>;
    fn record_success(&self, id: &str) -> Result<()>;
    fn record_failure(&self, id: &str) -> Result<()>;
}

Summary

Key Points:

  1. Use WebhookClient to send outgoing webhooks
  2. Use WebhookReceiver to verify incoming webhooks
  3. Use WebhookRegistry to manage multiple endpoints
  4. Always verify signatures before processing
  5. Implement idempotency for reliable processing
  6. Configure appropriate retry policies

Quick Start:

use armature_webhooks::{WebhookClient, WebhookConfig, WebhookPayload};

// Send a webhook
let client = WebhookClient::new(WebhookConfig::default());
let payload = WebhookPayload::new("user.created")
    .with_data(json!({"user_id": "123"}));
client.send("https://example.com/webhook", payload).await?;

// Receive a webhook
let receiver = WebhookReceiver::new("secret");
let payload = receiver.receive(&body, &signature)?;