Tech Blog

Getting Started with Webhooks in Alma

Now that Alma supports webhooks, a whole new style of integrating and orchestrating technical tasks is available to developers and system librarians. With webhooks, you can request that Alma make an HTTP callback when a certain event occurs. As Wikipedia describes, webhooks are “user-defined HTTP callbacks …  usually triggered by some event. … When that event occurs, the source site makes an HTTP request to the URI configured for the webhook.”

To enable webhooks in Alma, you configure a webhook integration profile. Once activated, Alma will send a message to the specified HTTP endpoint whenever certain events occur. Let’s take a look at an end-to-end flow leveraging webhooks in Alma.

Listener

We need to create an HTTP endpoint which Alma will call with the relevant information. In this example, we’ll create our listener in Node.js (for more information on using Node.js with Alma APIs, see this blog post). We configure our app.js with a route for webhooks and then add the route for the desired path:

var webhooks = require('./routes/webhooks');
app.use('/webhooks', webhooks);

Challenge

In our webhooks.js file, we need to handle both GET and POST. The GET method is used for the challenge phase of the initial setup. Alma sends a term and the endpoint is expected to echo the term back, allowing Alma to confirm that there is an endpoint listening that expects messages from Alma. The POST message accepts the actual webhook calls for each event defined in Alma.

To handle the challenge, we simply echo the term that was received as a querystring parameter:

router.get('/', function(req, res, next) {
   res.json({ challenge: req.query.challenge });
});

Event Handler

To handle the actual events from Alma, we implement the POST method. First we validate the signature to ensure the request came from Alma. Alma signs the body with a Base64-encoded HMAC-SHA256 signature using the secret configured in the webhook integration profile. We extract the value from the X-Exl-Signature header, compute the hash of the body using the same secret, and compare the values. If they don’t match, we respond with a 401 error code.

if (!validateSignature(req.body, secret, req.get('X-Exl-Signature'))) {
      return res.status(401).send({errorMessage: 'Invalid Signature'});
}

We use the method below to compute the hash:

function validateSignature(body, secret, signature) {
   var hash = crypto.createHmac('SHA256', secret)
      .update(JSON.stringify(body))
      .digest('base64');
   return (hash === signature);
}

If the signature matches, we extract the action value from the body and perform our business logic according to our requirements.

var action = req.body.action.toLowerCase();
switch (action) {
   default:
      console.log('No handler for type', action);
}

Configuring Alma

We now need to configure a webhook integration profile in Alma. We provide the URL of our application and a secret we want Alma to use to sign the requests. We activate the webhook listener and save the integration profile.

Putting it together

Once everything is configured we can run a job within Alma. When the job is completed, Alma will send a webhook to our endpoint. We don’t have any custom logic running at the moment, but we do see our log entry which shows the JSON Alma sent along with our message that no handler was found for “job_end”.

15:09:46.751745+00:00 app[web.1]: Received webhook request: {"id":"1560384420000561","action":"JOB_END","time":"2016-07-05T15:09:39Z","job_instance":{"id":"1560384420000561","name":"Export Bibliographic Records - Test small set - 05/07/2016 10:09:35 CDT","submitted_by":{"value":"exl_impl","desc":"Implementer, Ex Libris"},"submit_time":"2016-07-05T15:09:38Z","start_time":"2016-07-05T15:09:38Z","end_time":"2016-07-05T15:09:39Z","progress":100,"status":{"value":"COMPLETED_SUCCESS","desc":"Completed Successfully"},"status_date":"2016-07-05T00:00:00Z","category":{"value":"REPOSITORY","desc":"Repository"},"link":"/almaws/v1/conf/jobs/M0/instances/1560384420000561","alert":[{"value":"alert_general_success","desc":"The job completed successfully. For more information view the report details."}],"counter":[{"type":{"value":"c.jobs.bibExport.recordsProcessed","desc":"Records processed"},"value":"1"},{"type":{"value":"c.jobs.bibExport.recordsExported","desc":"Records exported"},"value":"1"},{"type":{"value":"c.jobs.bibExport.link","desc":"Link to Exported records"},"value":"BIBLIOGRAPHIC_1560384400000561"}]
15:09:46.752396+00:00 app[web.1]: No handler for type job_end
15:09:46.759729+00:00 app[web.1]: [0mPOST /webhooks [32m204 [0m52.474 ms - -[0m

This is a simple but powerful pattern which opens up lots of possibilities for integration scenarios. The code for our sample webhook listener is available in Github.

Github

Leave a Reply