Development

Webhook Support for Product Availability

The Requirement

Currently, the Make it Social Product API provides a production-ready booking service for experience providers with the following features.

  • It is a product repository accessible through a Restful API;
  • It has a builtin booking engine also accessible via Restful API;
  • It uses RRule to specify product availability by date and time;
  • And it supports group-size and vacancy control;
  • It notifies the product owner via webhook for booking status.

While this is enough for the Make it Social hosted products, it does not support those hosted on third-party booking engines.In our previous monolithic system, we built plugins to support as many third-party booking engines as necessary.But in the new PAPI system, we use a different approach, namely, the webhooks. Instead of calling various APIs with their specified data format, we let the third-party engines or a middle service to handle our requests.

The Webhooks

The client or a middleman service must listen on an endpoint for availability and booking requests by PAPI on behalf of the users.The requests will be a HTTP POST call to the endpoint, for example:

curl -X POST -d '{"start_date":"2016-01-01T09:30:00Z",..}' https://example.com/products/availability -H "Content-Type:application/json"

It’s product provider’s responsibility to configure these webhooks and setup a webservice to listen on these endpoints.This can be done by adding the following to either the provider or the product dataset.

{
	"webhooks": {
		"availability": "https://..",
		"prepayment": "https://..",
		"postpayment": "https://..",
		"shared_secret": ".."
	}
}

The shared_secret is a string to be used by PAPI to hash the post request for a signature.

Availability endpoint

This endpoint will be called when a user joins the activity.Minimal request data:The post body will be a JSON string like the following example.

{
	"provider_id": "..",
	"product_id": "..",
	"group_size": 2,
	"start_date": "yyyy-mm-ddTHH:MM:SSZ",
	"end_date": "yyyy-mm-ddTHH:MM:SSZ",
	"time_code": "",
	"rate_code": "",
	"hash": "hash(..)"
}

The product_id is the unique product ID on PAPI.

Expected response

{
	"available": true,
	"hash": "hash(..)"
}

or

{
	"available": false,
	"reason": "Error calling the booking engine",
	"hash": "hash(..)"
}

Pre-payment endpoint

This will be called immediately before the user starts the payment.

The Request

{
	"id": "booking_id",
	"provider_id": "..",
	"product_id": "..",
	"currency": "GBP",
	"amount": 199.99,
	"user": {
		"uid": "user_id",
		"first_name": "<first name>",
		"last_name": "<surname>",
		"email": "email@example.com",
		"address": [
			"s1": "street",
			"s2": "",
			"city": "City",
			"postcode": "postcode",
			"country": "GB"
		],
		"data_capture": {
			"name": "value"
		}
	}
	"group_id": "group_id",
	"start_date": "yyyy-mm-ddTHH:MM:SSZ",
	"end_date": "..",
	"group_size": 2,
	"rate_code": "",
	"time_code": "",
	"voucher": "",
	"extras": [
		{
			"xid": "product_id",
			"name": "product name",
			"price": 10.00
		}
	],
	"timestamp": 1445526011477,
	"hash": "hash(..)"
}

Expected Response

The webhook handler should return with an availability status, and a booking_id if done so. The booking_id will be used for confirm or cancel after payment.

{
	"available": true,
	"reserved": 1,
	"booking_id": "",
	"expire_at": "ISO time",
	"hash": "hash(..)"
}

The reserved and expire_at properties are optional.The response for failed reservation:

{
	"available": false, 
	"reason": "No longer available",
	"hash": "hash(..)"
}

Post-payment endpoint

This will be called after a successful or failed payment. To confirm a successful payment, the request body will contain a property action:”CONFIRM”, otherwise, action:”CANCEL”.

Request data

The request body will be the same as that for pre-payment, plus “prepayment”: {} from the response of pre-payment call if done.

{
	"action": "CONFIRM",
	"prepayment": {
		"booking_id": "1234"
	}
	...
}

Expected response

{
	"status": "OK",
	"ref_no": "the reference number",
	"link": "URL for user to collect the ticket or to read the guide",
	"hash": "hash(..)
}

or for failure:

{
	"status": "FAILED",
	"reason": "..",
	"hash": "hash(..)
}

Note on pre- and post-payment endpoints

It is not necessary to have both depending on the booking engine. But at least one should be available for booking. If pre-payment booking (reservation) is available, then a booking_id in any format must be available so that later confirmation or cancellation can use as the ID, unless confirmation or cancellation are not necessary.

The Signature

Post to the endpoint as well response data received from the webhook should have a signature as a hash of the data content and a shared secret.
The hashing content is a string concatenation of all the properties flattened into a key=value array. For example:

{
	"action": "CONFIRM",
	"prepayment": {
		"booking_id": "a1223"
	}
}

will be converted into the following array of strings.

 ["action=CONFIRM","booking_id=a1223"]

And then concatenate the strings together and append the shared secret to it, and then hash it using sha256 and encode the result in base64.The signature generation function can be something like this:

function hash(obj, secret) {
	var buf = [];
	function flattern(obj){
		for (var k in obj) {
			var v = obj[k];
			if (typeof(v)=='object') {
				flattern(v);
			} else {
				buf.push(k+'='+obj[k]);
			}
		}
	}
	flattern(obj);
	buf.sort();
	buf.push(secret);
	return crypto.createHash('sha256')
		.update(buf.join(''))
		.digest('base64');
}

Leave a Reply

Your email address will not be published. Required fields are marked *