Build a Stripe webhook handler with Functions
Receive Stripe payment events on a Hono app deployed as a function, verify every signature, and handle each event type.
In this tutorial, you will build a Stripe webhook handler that runs as a function on Azion. You will create the project, verify the signature, handle the events, store the credentials, deploy the handler, and register its URL with Stripe.
The handler is a Hono app with two routes: POST /webhook, which receives the events, and GET /, which reports the status of the service.
Prerequisites
- An Azion account. To create one, refer to How to create an account on Azion.
- A Stripe account with API access.
- Azion CLI installed.
- Stripe CLI installed.
- Node.js 18 or higher.
1. Create the project
Azion CLI scaffolds the project from the Hono preset. To create it:
Run the login command:
The command opens a browser-based flow and stores the resulting personal token locally, so the next commands run against your account.
Run the init command:
Enter a name, or press enter to accept the suggestion the CLI prints.
The CLI creates the project directory with the Hono template and its configuration files.
2. Verify the webhook signature
Stripe signs every webhook request and carries the signature in the stripe-signature header. A handler that acts on an unverified request acts on any request that reaches its URL, so the signature check runs before the route.
The middleware below rejects a request with no signature, rejects a request whose signature does not match, and attaches the parsed event to the Hono context for the route handler.
The entry field of azion.config.js names the entry file of the project. Open that file and replace its contents with the Stripe client and the verification middleware:
A request that fails the check receives a 400 response and never reaches the route.
3. Handle the payment events
The route reads the verified event from the context and branches on its type. Every branch ends in a 200 response: Stripe retries an event that the endpoint does not acknowledge. A retry delivers an event the handler may have already acted on, so record the id of every event it processes and skip one that repeats. A retry can redeliver an event the handler already processed, so store each event.id and skip an ID already stored.
Add the webhook route, the status route, the error handler, and the export to the same file:
The handler answers every verified event with { "received": true }, and an event type outside the switch reaches the default branch and is logged.
For the reference implementation of this handler, refer to edge-functions-examples.
4. Store the Stripe credentials
The handler reads both Stripe keys from the environment, so neither value belongs in the code. The secret key starts with sk_test_ or sk_live_, and the webhook signing secret starts with whsec_.
Store the secret key as an environment variable on your account:
Store the webhook signing secret the same way:
Both variables are stored on the account with the secret field set to true, which marks the value as confidential. A variable whose key contains password, pwd, secret, key, hash, encrypted, passcode, auth, or token is sent as a secret by default. A change to a variable reaches the function only after a redeploy. For the fields, the limits, and the other subcommands, refer to Environment variables.
5. Deploy the handler
Deploy the project:
Azion CLI builds the project, deploys it, and opens Azion Console on the page that carries the deployment logs. When the browser does not open, the terminal prints the link.
The deployment returns a domain in the format https://xxxxxxxxx.map.azionedge.net. Propagation takes a few minutes, so wait before you send the first event. The webhook route of the handler is /webhook on that domain.
6. Point Stripe at the handler
To deliver the events to the deployed handler:
In the Stripe Dashboard, go to Developers > Webhooks.
Enter https://<your-azion-domain>/webhook.
Stripe delivers to your function every event selected on that endpoint. For the event types an endpoint accepts, refer to Stripe webhooks.
7. Verify the handler
To exercise the deployed handler:
The route returns a JSON object with three fields: status set to ok, timestamp set to the time of the request, and service set to stripe-webhooks.
Stripe delivers each event to the registered endpoint, and the handler answers { "received": true }. The Stripe Dashboard lists the delivery, its response code, and any retry for that endpoint.
The handler answers { "received": true } again, and the event reaches the default branch.
The terminal prints the console messages of the last 5 minutes and keeps printing new ones.
Each triggered event produces one line: PaymentIntent was successful! with the payment intent ID, Charge was successful! with the charge ID, and Unhandled event type invoice.payment_succeeded for the event the switch does not name.