Webhooks
Handle Stripe webhook events securely in your application
Stripe uses webhooks to notify your application when events happen in your account. async-stripe provides utilities to securely verify and parse webhook events.
Stripe Webhooks Guide
Official Stripe documentation on webhooks
Testing Webhooks
Learn how to test webhooks with the Stripe CLI
Overview
Webhooks allow Stripe to push real-time notifications to your application when events occur, such as successful payments, failed charges, or subscription updates. This is more reliable than polling the API and ensures you can respond to events immediately.
Webhook Signature Verification
Stripe signs webhook payloads with a secret key to prevent tampering and spoofing. You must verify the signature before processing any webhook event.
Always verify webhook signatures in production. Processing unverified webhooks is a security risk.
Using the Webhook Module
The stripe_webhook crate provides the Webhook::construct_event function to verify and parse webhook events in one step:
use stripe_webhook::{Event, Webhook};
let event = Webhook::construct_event(
&payload, // Raw request body as string
&signature_header, // stripe-signature header value
"whsec_xxxxx" // Your webhook signing secret
)?;Framework Examples
async-stripe includes webhook examples for popular Rust web frameworks:
Axum
Webhook handling with Axum using a custom extractor
Rocket
Webhook handling with Rocket using request guards
Actix Web
Webhook handling with Actix Web
Axum Integration
Here's a complete example of handling webhooks in an Axum application with automatic signature verification:
struct StripeEvent(Event);
impl<S> FromRequest<S> for StripeEvent
where
String: FromRequest<S>,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(req: Request<Body>, state: &S) -> Result<Self, Self::Rejection> {
let signature = if let Some(sig) = req.headers().get("stripe-signature") {
sig.to_owned()
} else {
return Err(StatusCode::BAD_REQUEST.into_response());
};
let payload =
String::from_request(req, state).await.map_err(IntoResponse::into_response)?;
Ok(Self(
Webhook::construct_event(&payload, signature.to_str().unwrap(), "whsec_xxxxx")
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?,
))
}
}The StripeEvent extractor automatically:
- Extracts the
stripe-signatureheader - Reads the raw request body
- Verifies the signature
- Parses the event
- Returns a 400 Bad Request if verification fails
Handling Events
Once you have a verified event, pattern match on the event type:
#[axum::debug_handler]
async fn handle_webhook(StripeEvent(event): StripeEvent) {
match event.data.object {
EventObject::CheckoutSessionCompleted(session) => {
println!("Received checkout session completed webhook with id: {:?}", session.id);
}
EventObject::AccountUpdated(account) => {
println!("Received account updated webhook for account: {:?}", account.id);
}
_ => println!("Unknown event encountered in webhook: {:?}", event.type_),
}
}Testing Webhooks Locally
Use the Stripe CLI to forward webhook events to your local development server:
# Install the Stripe CLI
# https://stripe.com/docs/stripe-cli
# Forward events to your local endpoint
stripe listen --forward-to localhost:4242/stripe_webhooks
# In another terminal, trigger test events
stripe trigger checkout.session.completed
stripe trigger payment_intent.succeededBypassing Signature Verification (Development Only)
For early-stage local testing where you don't want to set up the Stripe CLI or need to test with manually crafted payloads, you can use the insecure() method to bypass signature verification:
use stripe_webhook::{Event, Webhook};
// ONLY use this in development/testing environments
let event = Webhook::insecure(payload)?;Security Warning
Never use Webhook::insecure() in production. This method completely bypasses signature verification, making your application vulnerable to webhook spoofing and tampering. Always use construct_event() with proper signature verification in production environments.
This is useful for:
- Local development without the Stripe CLI
- Unit testing with fixture data
- Integration testing in CI/CD environments
For production-like testing, always use the Stripe CLI with proper webhook secrets.
Event Types
The EventObject enum contains variants for all Stripe webhook events. Common event types include:
CheckoutSessionCompleted- A Checkout Session was successfully completedPaymentIntentSucceeded- A PaymentIntent successfully completedPaymentIntentFailed- A PaymentIntent failedCustomerCreated- A new customer was createdInvoicePaid- An invoice was successfully paidSubscriptionCreated/SubscriptionUpdated/SubscriptionDeleted- Subscription lifecycle events
For a complete list of all available event types, see the Stripe Event Types documentation.
Not all Stripe events may be represented in the EventObject enum yet. Use the catch-all pattern _ to handle unknown events gracefully.
Best Practices
Return 200 Quickly
Stripe expects a 200 response within a few seconds. Process webhooks asynchronously if they require long-running operations:
async fn handle_webhook(StripeEvent(event): StripeEvent) -> StatusCode {
// Spawn background task for heavy processing
tokio::spawn(async move {
process_event(event).await;
});
// Return immediately
StatusCode::OK
}Handle Idempotency
Stripe may send the same webhook multiple times. Use the event.id to track processed events and avoid duplicate processing:
async fn handle_webhook(StripeEvent(event): StripeEvent) {
if already_processed(&event.id).await {
return; // Skip duplicate
}
process_event(event).await;
mark_as_processed(&event.id).await;
}Secure Your Endpoint
- Use HTTPS in production
- Keep your webhook signing secret secure (use environment variables)
- Verify signatures on every request
- Return appropriate status codes (200 for success, 400 for bad requests)
- Log webhook failures for debugging
Other Web Frameworks
While this guide shows Axum integration, the same principles apply to other frameworks:
- Extract the
stripe-signatureheader - Read the raw request body as a string
- Call
Webhook::construct_eventwith the payload, signature, and secret - Return 400 if verification fails, 200 on success