Testing and debugging webhooks with ngrok
Tags: tutorial, tooling, webdev
Published: 2021-02-07 20:51:52

Let's say you're building a web application that receives webhooks from external services like [GitHub](https://docs.github.com/en/developers/webhooks-and-events/about-webhooks), [Stripe](https://stripe.com/docs/webhooks) or others. These webhooks work by sending HTTP POST requests to an endpoint on your server, allowing you to listen for events as they happen without having to continuously poll for new data. If you're developing locally though, you won't be able to receive webhooks at `http://localhost:8000` (or any other local URL!) This is where [ngrok](https://ngrok.com/) comes in, allowing you to create a temporary public URL to tunnel HTTP traffic to your local application.

In this tutorial I'll walk through installing ngrok and using it to test out the [Stripe](https://stripe.com/docs/webhooks) webhook, but this can work for any webhook you need to work with!

## Installing ngrok

1. Head over to [ngrok](https://ngrok.com/) and sign up for an account. You don't really need one for this tutorial so you can skip this step if you want, but having an account gives you a few extra features (like longer session times)

2. On macOS, ngrok can be installed via Homebrew: `brew install --cask ngrok`. You can also download an executable for macOS, Windows or Linux [directly from their website](https://ngrok.com/download). Keep track of where it's been downloaded so you'll know which path to access it at after. 

3. Try running it from a terminal window to make sure it's working: `ngrok help`. 

The syntax for tunneling HTTP traffic to a port on your local computer is `ngrok http <port_to_tunnel_to>`. For example, if I wanted to tunnel HTTP requests to a server listening for requests at port 5000 on my local machine, I would type `ngrok http 5000` in a shell:

![Typing ngrok http 5000 in my terminal to start a tunnel to port 5000 on my machine](https://dev-to-uploads.s3.amazonaws.com/i/o9pyculgbd3v3bm5cy78.gif)

You'll notice the `Forwarding: https://11d51c640b97.ngrok.io -> http://localhost:5000`. This means all requests to `https://11d51c640b97.ngrok.io` will be forwarded to port 5000 on my local machine, where my application can receive them! You can also access a request inspector in a browser at `http://127.0.0.1:4040`, where you can see (and even replay) any HTTP requests that went through. Press Ctrl+C whenever you're done and the tunnel will be closed.

## Using ngrok to test the Stripe webhook

In this example I'm going to use a Flask application that's listening for incoming webhooks from Stripe at `/webhooks/stripe`. Let's say I'm interested in `charge.succeeded` events, because these tell me someone has paid for something on my fictitious platform! If you want to follow along with this tutorial, all the code is available on GitHub. Set up your local environment and install a few dependencies (ensure Pipenv is installed first):

```shell
git clone https://github.com/mm/stripe-ngrok-example.git
cd stripe-ngrok-example
pipenv install
```

Let's take a look at the code in `app.py`, which defines the endpoint that's listening for events:

```python
from flask import Flask, request, jsonify

app = Flask(__name__)


@app.route('/webhooks/stripe', methods=['POST'])
def receive_stripe_webhook():
    """Receives a webhook payload from Stripe.
    """
    
    # Try to parse a webhook payload, get upset if we couldn't
    # parse any JSON in the body:
    stripe_payload = request.json
    if not stripe_payload:
        return jsonify(message="Could not parse webhook payload"), 400

    event = stripe_payload.get('type')
    if not event:
        return jsonify(message="Could not determine event type"), 400
    if event == 'charge.succeeded':
        # Pull fields out of payload:
        data_object = stripe_payload.get('data').get('object')
        customer_id = data_object.get('customer')
        amount = data_object.get('amount')
        # Here we just log the transaction, but at this point we can do
        # anything! (Provision accounts, push to a database, etc.)
        print(f'Customer {customer_id} made a purchase of {amount} cents!')       

    return jsonify(message="Webhook received"), 200
```

Besides a bit of input data handling, all this code is doing is waiting for a `POST` request to `/webhooks/stripe`, checking for a `charge.succeeded` event (we successfully charged someone on our platform), and logging a bit of information about the charge to the console. You can start a local Flask server by running `pipenv run flask run` in a shell from the project directory. The server will let you know where it's listening for new requests -- in my case, `http://127.0.0.1:5000/`. Let's expose this via ngrok and have Stripe send us some data!

While the server's running, in a new terminal window type `ngrok http 5000` (or whatever port the Flask app is listening at):

![After starting ngrok, I have access to a forwarding URL -- https://07136e03dd33.ngrok.io](https://dev-to-uploads.s3.amazonaws.com/i/cvu1hhfkykp2t0of3s4v.png)

Notice the forwarding URL generated by ngrok. In my case, mine is `https://07136e03dd33.ngrok.io`. Now, let's have Stripe send data here:

1. Log in to your Stripe account (here I'm using a test account)

2. In the left sidebar, click on "Developers", then "Webhooks".

3. Under "Endpoints", click "+ Add Endpoint". The endpoint URL is the forwarding URL ngrok gave you (plus whatever path your server is listening at). In my case, it's `https://07136e03dd33.ngrok.io/webhooks/stripe` (this would be equivalent to `http://localhost:5000/webhooks/stripe`)

    ![Adding my webhook endpoint in the Stripe console](https://dev-to-uploads.s3.amazonaws.com/i/x51bshz9woie2qq2s01l.png)

    Here I set it up so I only receive `charge.succeeded` events, but there's all kinds you can listen for in the [Stripe docs](https://stripe.com/docs/api/events) (my favourite docs ever)

4. Next, record a charge for a customer. I'm in a test environment, so I'll just record a charge for a test customer named after me:

    ![Recording a payment](https://dev-to-uploads.s3.amazonaws.com/i/9z4va2q5auqf3xw719bp.png)

5. If you've been checking your Flask server, you'll notice we received a request to our endpoint in the logs:

    ```shell
    Customer cus_Itm097l6URXxaZ made a purchase of 400 cents!
    127.0.0.1 - - [06/Feb/2021 16:39:42] "POST /webhooks/stripe HTTP/1.1" 200 -
    ```


6. Head over to `http://localhost:4040` in your browser (or whatever URL ngrok gave you for your "Web Interface") Here, you'll see a log of the request made to your local app:

    ![Seeing the request log from ngrok in my browser](https://dev-to-uploads.s3.amazonaws.com/i/qhgkf58av7yiul52smal.png)

7. Let's say you make some changes to your code and want to test the same payload against the new code. You don't need to record another payment in Stripe! You can hit the "Replay" button in the ngrok inspector and replay the request (or even modify it before replaying). This can help you test code faster without adding huge amounts of data to your test environment (be it Stripe or any other)

8. Hit Ctrl+C on both your ngrok session and Flask server once you want to stop receiving requests. That's all there is to it.

## Wrapping up

In this tutorial, we covered:

* How to tunnel HTTP requests to your local server to test webhooks with ngrok
* Using the ngrok inspector to inspect and replay requests to your server without needing to generate more events

Let me know if it was helpful in testing your own applications. Here we covered testing the Stripe webhook this way, but this strategy can work for other webhooks too!

*Header photo by [Paul Carmona](https://unsplash.com/@sydneylens?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/spiderweb?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)*