Adding webhooks to your application

August 30, 2011 Pete Keen

We’ve been working hard over the last few months to release a new version of Emma, one built on top of an entirely new platform that will help our customers get the most out of their subscription. One of the things we recently added was a suite of webhooks, which will allow Emma to tell API users when an event occurs without them having to poll us continually.

For those of you who don’t know what a webhook is, it’s basically a jazzed up HTTP POST that a web application sends to a URL that a user has registered. Several of the popular code and project hosting sites allow users to set up webhooks that get called when someone makes a commit. The Emma webhooks are not much different, but we potentially have a much larger suite of events a user will be able to register for.

It turns out that adding webhooks to your application is actually pretty trivial if you have these bits already:

  • User/API authentication
  • An asyncronous processing system like Resque or Celery

The bare minimum you need to provide to users is an API they can use to register a new webhook. These are just your normal every day CRUD APIs that operate on a really simple model that, in SQLAlchemy, would look like this:

class Webhook(Base):<br />
    __tablename__ = 'webhooks'</p>
<p>    webhook_id = Column(BigInteger, primary_key=True)<br />
    user_id = Column(BigInteger, nullable=False,<br />
        ForeignKey('users.user_id'))<br />
    url = Column(Text, nullable=False)

So now you’ve got a bunch of webhooks. Let’s say our webhook will fire when we send an email. In a real system you’d want to be more discriminating, passing around user_id and only firing off the appropriate hooks.

Where you define your background tasks, you’d do something like this (using Celery):

import requests<br />
import json<br />
from celery import task</p>
<p>@task<br />
def send_webhook(to, contents):<br />
    webhooks = session.query(Webhook)</p>
<p>    for webhook in webhooks:<br />
        data = {'user_id': webhook.user_id,<br />
            'to': to, 'body': contents}<br />,<br />
<p>@task<br />
def send_an_email(to, contents):<br />
    (from, to, msg) = build_email_message(<br />
            to, contents)<br />
    smtplib.SMTP().sendmail(from, to, msg)<br />
    send_webhook.delay(to,<br />
            contents) # fire off the webhook soon<br />

This sample also uses the excellent Requests library, which wraps all of the urllib and urllib2 madness in an extremely easy to use library. I highly recommend it.

Now, our little application will send off webhooks whenever we send an email. How do our users know that it’s a) from us, and b) for them? Let’s add some HMAC-MD5 signing:

from base64 import b64encode<br />
import hashlib<br />
import hmac</p>
<p>@task<br />
def send_webhook(to, contents):<br />
    webhooks = session.query(Webhook)</p>
<p>    for webhook in webhooks:<br />
        data = json.dumps({<br />
            'user_id': webhook.user_id,<br />
            'to': to,<br />
            'body': contents})</p>
<p>        user = session.query(User).\<br />
            get(webhook.user_id)<br />
        digestor =<br />
            bytes(user.api_key),<br />
            bytes(data),<br />
            hashlib.md5)<br />
        digest = b64encode(digestor.digest())</p>
<p>, data,<br />
            headers={&quot;Webhook-Hmac-MD5&quot;: digest})<br />

Users can authenticate the message by signing the POST request body they get from you using the same method and comparing it to the Webhook-Hmac-MD5 header.

So, webhooks are pretty cool. They let users do fun, unexpected things with your app without much effort on your part. Using Requests and your favorite background task system, they’re also almost trivial to add to an existing system. If you’ve got experience with webhooks, I’d love to hear about it. Comment below to get the dialogue going.

Previous Article
Usability testing at Emma

There’s a lot of talk these days about being a user experience designer — what it means, who does it, who d...

Next Article

Last Wednesday, our new API officially entered private beta, swinging the doors open to early adopters. To ...