If you haven’t heard of Zapier before, its a service that lets you pipe and integrate 3rd-pary APIs. Lets say you have an ecommerce site and when orders are placed, you want to do two things:

  1. Notify staff via email
  2. Call supplier

With Zapier you can create what’s called a Zap, which is basically a series of steps that needs to be done when an event (trigger) happens. In your Zap, you can create a custom app that will send a trigger to Zapier when an order is placed and pipe the actions send email to staff and call supplier. Zapier have some built-in apps for sending email and a lot of 3rd-party apps like Twilio for making phone call.

So how do you build an app so you could integrate against APIs that are already available on Zapier? There are two ways, you can either use the web builder or the Zapier CLI. There are pros and cons for both, which you can read more about here but for our example we’re going to use Zapier CLI.

Before we start building though, we first need to decide of a method on how we should notify Zapier of events. Zapier supporst polling and resthooks, polling is really inefficient so we’re going to use resthooks instead. If you’re not familiar with resthooks, you can read more about it here but basically its a more efficient alternative to polling.

First we need to install Zapier CLI:

$ npm install -g zapier-platform-cli

Zapier has an app template for resthooks project, so we’re going to use that:

$ zapier init ./MyShopApp --template=rest-hooks

We’ll end up with a directory like this:

MyShopApp
├── index.js
├── package.json
├── test
│   └── triggers.js
└── triggers
    └── recipe.js

Our app is defined at index.js, update it to look like:

const order = require('./triggers/order');

const App = {
  ...

  triggers: {
    [order.key]: order
  },

  ...
};

module.exports = App;

This is how we define our trigger, which is what defines how we inform Zapier about newly created orders. See https://zapier.github.io/zapier-platform-schema/build/schema.html#appschema for more info.

Next rename triggers/recipe.js into triggers/order.js:

$ mv triggers/recipe.js triggers/order.js

There’s a bit of code on this file so lets go through it piece-by-piece, the basic structure of resthook trigger looks like:

{
  key: 'order',
  noun: 'Order',
  display: {
    label: 'New Order',
    description: 'Trigger when a new order is created.'
  },

  operation: {
    type: 'hook',

    // any additional input you'd like to ask from the user,
    // fields listing here will show-up on zap editor when
    // user is setting-up the zap
    inputFields: [...],

    // see below for usage
    performSubscribe: subscribeHook,
    performUnsubscribe: unsubscribeHook,

    // called after notifying Zapier of new order
    perform: getOrder,

    // called when fetching sample data, also acts as polling fallback
    performList: getFallbackOrders,

    // how sample data should look like
    sample: {...},

    // what fields to expect from data we post to Zapier
    outputFields: [...]
  }
}

Here, I wen’t ahead and update the key to order and noun to Order. Also update the display.label and display.description to properly describe when our trigger gets triggered.

The way this trigger works is that when user turns-on the zap from zap editor, our trigger’s performSubscribe will get called where a bundle.targetUrl and any user input (bundle.inputData) will be passed. Our subscribe hook function then needs to store that information and return an object that contains the subscripion identifier.

When the zap is turned-off the performUnsubscribe will get called, passing in whatever data the subscribe hook previously returned (bundle.subscribeData). We then use the subscription identifier to delete the record about the subscription that we previously stored.

I’ve added comments for the other fields, but also see https://zapier.github.io/zapier-platform-schema/build/schema.html#basichookoperationschema for more info about the fields.

Next update subscribeHook to look like:

const subscribeHook = (z, bundle) => {
  const data = {
    url: bundle.targetUrl,
    event: 'order_created'
  };

  const options = {
    url: 'http://myshopapp.io/api/subscribe',
    method: 'POST',
    json: data
  };

  return z.request(options)
    .then((resp) => JSON.parse(resp.content));
};

subscribeHook, unsubscribeHook, perform and performList receive the same parameters, first one is the z object which contains helper methods like logging, making http requests, etc. The second param is bundle data passed around like authentication, input data, etc.

Remember earlier how I said we need to keep track of the subscription by storing it, here we are posting the data to our own api so we could save it on our database. Ensuring that after storing the subscription, it returns an object along with the subscription identifier. Its also easier to just return the newly saved subscription.

What about the unsubscribeHook?

const unsubscribeHook = (z, bundle) => {
  const subscriptionId = bundle.subscribeData.id;
  const options = {
    url: `http://myshopapp.io/api/unsubscribe/${subscriptionId}`,
    method: 'DELETE',
  };

  return z.request(options)
    .then((response) => JSON.parse(response.content));
};

Here bundle.subscribeData will contain whatever object the subscribe hook returned, so from there we just grab the subscriptionId which we then use to send to our api a delete request. On our api, presumably we use the subscriptionId to delete the subscription from our database.

Now we need to update our getFallbackOrders:

const getFallbackOrders = (z, bundle) => {
  const options = {
    url: 'http://myshopapp.io/api/orders',
    method: 'GET'
  };

  return z.request(options)
    .then((resp) => JSON.parse(resp.content));
};

Here we’re just fetching orders for the fallback and alse when fetching sample data. So presumably your api will return limited number of orders only ordered by the latest.

And finally our getOrder function:

const getRecipe = (z, bundle) => {
  return [ bundle.cleanedRequest ];
};

Nothing special here, we just want to return whatever data we posted to Zapier. One important thing is that this should return an array even if its just one element.

After all that, we can now register our app:

$ zapier register "My Shop App"

and then push our app so we can use it in zap editor:

$ zapier push

Now our new app will be available on zap editor, but I also like to hightlight some things on our API’s side that we need to do when notifying Zapier.

When an order is placed, we get all order_created subscriptions we previously stored for the current user who made the order. For each subscription, we then post the new order to the targetUrl for that subscription. And if after posting to a targetUrl the response returns 410, we need to delete that subscription. Here’s how it might look like:

class OrderService {
  async createOrder() {...}

  async unsubscribe() {...}

  async notifySubscription(order, subscription) {
    try {
      const resp = await request({
        url: subscription.url,
        method: 'POST',
        json: order
      });

      if (resp.statusCode === 410) {
        await this.unsubscibe(subscription);
      }
    } catch (err) {
      console.error(err);
    }
  }

  async notifyZapier(order) {
    const subscriptions = await this.subscriptionRepo.find({
      userId: this.user.id,
      event: 'order_created'
    });
    return Promise.all(subscriptions.map(async (subscription) => {
      await this.notifySubscription(order, subscription);
    }));
  }
}

...

const order = await orderService.createOrder();
orderService.notifyZapier(order);

A bit of a pseudocode but you get the idea, and that’s it, you should have a working Zapier app with resthook trigger. Happy Zapping!