What is a webhook and how to create one?

By Karolis Rusenas · Jul 13, 2018

As most of what we do on the web can be described by events, webhooks have become a de facto way of connecting systems, whether it’s a new user account creation, successful payment, DockerHub image push event or a new pull request in your Git repository. They are incredibly useful and lightweight way of sharing information. Let’s explore the meaning of webhooks and how they can help you.

webhook workflow example

So, what exactly is a webhook? A webhook is a way for an app to provide other applications with real-time information. A webhook delivers data to other applications as it happens, meaning you get data immediately. Unlike typical APIs where you would need to poll for data very frequently in order to get it real-time. This makes webhooks much more efficient for both provider and consumer. And as most of the providers have strict API rate limits, you will either have to adopt webhooks or accept that your system will always be slightly (or more) out-of-date. The example above shows a typical flow for requesting payments from the users.

Webhooks are sometimes referred to as “Reverse APIs,” as they give you what amounts to an API spec, and you must design an API for the webhook to use. The webhook will make an HTTP request to your app (typically a POST), and you will then be charged with interpreting it. You can think of it as a regular API route that your CLI/UI uses but in this case the user will be 3rd party service that you rely on.

While most developers are consuming webhooks that are produced by popular services such as Stripe or GitHub, sometimes you also want to let users receive webhooks from your application. In this blog post, we will build a simple application where users can register their endpoints and receive events.

If you would like to see learn how to handle incoming webhook in your application, check our tutorial instead.

Creating a webhook dispatching application

Our application will be written in Go (installation instruction can be found in the golang.org install page) but you can choose any language you want and this is just a simple example that demonstrates the outgoing webhook behavior. The code is simple and even non-Go developers should find it readable:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "sync"
    "time"
)

// port - default port to start application on
const port = ":8090"

type WebhookRequest struct {
    Name        string
    Destination string
}

func main() {

    dispatcher := &Dispatcher{
        client:       &http.Client{},
        destinations: make(map[string]string),
        mu:           &sync.Mutex{},
    }

    // preparing HTTP server
    srv := &http.Server{Addr: port, Handler: http.DefaultServeMux}

    // webhook registration handler
    http.HandleFunc("/webhooks", func(resp http.ResponseWriter, req *http.Request) {
        dec := json.NewDecoder(req.Body)
        var wr WebhookRequest
        err := dec.Decode(&wr)
        if err != nil {
            resp.WriteHeader(http.StatusBadRequest)
            return
        }

        dispatcher.add(wr.Name, wr.Destination)
    })

    // start dispatching webhooks
    go dispatcher.Start()

    fmt.Printf("Create webhooks on http://localhost%s/webhooks \n", port)
    // starting server
    err := srv.ListenAndServe()

    if err != http.ErrServerClosed {
        log.Fatalf("listen: %s\n", err)
    }
}

type Dispatcher struct {
    client       *http.Client
    destinations map[string]string
    mu           *sync.Mutex
}

func (d *Dispatcher) Start() {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            d.dispatch()
        }
    }
}

func (d *Dispatcher) add(name, destination string) {
    d.mu.Lock()
    d.destinations[name] = destination
    d.mu.Unlock()
}

func (d *Dispatcher) dispatch() {
    d.mu.Lock()
    defer d.mu.Unlock()
    for user, destination := range d.destinations {
        go func(user, destination string) {
            req, err := http.NewRequest("POST", destination, bytes.NewBufferString(fmt.Sprintf("Hello %s, current time is %s", user, time.Now().String())))
            if err != nil {
                // probably don't allow creating invalid destinations
                return
            }

            resp, err := d.client.Do(req)
            if err != nil {
                // should probably check response status code and retry if it's timeout or 500
                return
            }

            fmt.Printf("Webhook to '%s' dispatched, response code: %d \n", destination, resp.StatusCode)

        }(user, destination)
    }
}

Running your webhook producer

To use our webhook producer we need an endpoint that can receive and help us debug it. For this task, we will be using this free service: https://bin.webhookrelay.com/. Upon opening the link you should be redirected to a unique URL that is your bin. Get the bin address, you will need it in a minute.

Let’s start our application:

$ go run main.go
Create webhooks on http://localhost:8090/webhooks

Now, add our bin:

curl --request POST \
  --url http://localhost:8090/webhooks \
  --header 'content-type: application/json' \
  --data '{
    "name": "joe",
    "destination": "https://bin.webhookrelay.com/v1/webhooks/821024d7-12a0-4b41-99f2-71fcc2906989"
}'

After adding it, in a few seconds you should start seeing logs:

running app

And in our webhooks bin we should see incoming requests:

webhooks bin

Conclusion

To wrap up - webhooks and API requests are similar, both are events and the intent is to share information between systems. While API polling might look like a nice solution early on, having too many users relying on it could bring your backend down or considerably affect performance.

To successfully implement webhooks in your system, consider:

  • User should be able to specify a destination of the webhook
  • While most systems only allow a single destination to be added for webhooks, you might want to allow more.
  • Retry webhooks if the status code is >=500. While some might argue that we should retry with 4XX status codes, they usually mean that it’s a bad request and we shouldn’t retry the same request.

Interested or working with webhooks? Check out how you can receive webhook on localhost or private networks in our tutorials section. Webhook Relay has a free tier for developers!