Webhooks¶
A webhook is a mechanism for receiving updates about resources without periodically requesting the resources (known as polling). This solves important real-time issues, like having the most recent availability and prices to display to your customers and avoids unnecessarily consuming server resources.
To support webhooks, the developer must register an endpoint (URL) with the Application. This URL will be sent POST requests from the G Adventures API. Webhooks can be registered by editing the Application and updating the webhooks settings.
Note
Currently a webhook registration is for ALL event types (i.e. all or nothing approach). If you would like specific subscriptions, please let us know.
Common Uses
Maintaining accurate departure data (availability and prices)
Updating tour dossier descriptions and images
Finding out immediately when a new promotion is available
booking-related updates from G Adventures
Knowing when voucher documents are available
Knowing when a refund has been issued
Knowing when customer information has been changed
Keeping meta-resources such as country, state, or nationality in sync
A webhook is fired whenever the representation of a resource changes. Thus,
if a departures
resource changes, it will not necessarily deliver the
“parent” resource, tour_dossiers
unless the change in that departure
caused a change in the tour_dossiers
resource.
Additionally, below is a table containing common business needs, and the collection of webhooks you’d want to consider. Be aware that this is a subset and is only meant to provide an example of how you’d approach capturing webhooks.
Below, a table containing a realistic use-case, along with the webhooks you’d ideally listen to.
Use Cases |
Webhooks Collection |
Displaying a tour, its itineraries & dates. The price shown is a single “lead” price and no specific dates and availability are displayed. |
tour_dossiers, itineraries |
Displaying a tour/itinerary as above, but includes departure dates including availability, pricing per departure |
tour_dossiers, itineraries, departures |
As above, but includes promotional pricing |
tour_dossiers, itineraries, departures (All Promotion data is displayed in the departure) |
Collecting customer data and displaying it in reports, booking systems, etc. |
customers |
Collecting any booking data attributed to your agency |
customers, bookings, departure_services (And all other *_services as you need), payments |
Creating a form for your customers to checkout and purchase a G Adventures Trip |
countries, states, nationalities (Form meta data) |
Common Webhook Patterns¶
Receiving a Webhook¶
Configuring your server to receive a new webhook is no different than creating any other page on your site. With most frameworks, you’d add a new route with the desired URL and parse the request’s data.
Webhook data is sent as JSON in the request’s body. We advise you to guard against replay-attacks by recording which events you receive and never processing them twice.
An example webhook view using the popular Flask framework for Python might look something like this:
@application.route('/webhooks', methods=['POST'])
def webhooks_receiver():
# .. do something with request.json
return "OK", 200
Registering a Webhook¶
When editing your Application, you have the option to enter a Webhook URL. In order to verify the URL, you must respond to the POST request accordingly.
First, the response from the URL must be of 200
OK status code. Next, you
must include a header named X-Application-SHA256
containing the SHA256
hex digest of your live application key. To get this value, you can simply
visit your application dashboard, or compute it using your language’s hash
functions.
When entering a new webhook, you have the option to pick your preferred representation format. All webhooks to this callback will be sent to the selected format.
Responding to a Webhook¶
Your application, if it successfully handles the POST, must reply with a
200
OK status code. If it does not, the post will be retried for a maximum
of 72 hours before the job is abandoned.
Verifying a Webhook¶
Webhooks delivered by the G API can be verified by inspecting the
X-Gapi-Signature
that is part of each web hook request. To verify the
request came from the G API, compute the HMAC digest according to the following
algorithm and compare it to the value in the X-Gapi-Signature
header. If
they match, you can be sure the web hook came from the G API.
Here’s an example of how one might do that in Python:
import hashlib
import hmac
from flask import request
APPLICATION_KEY = 'MY_SECRET_APPLICATION_KEY'
@application.route('/webhooks', methods=['POST'])
def webhooks_receiver():
data = request.get_data()
calculated_hmac = hmac.new(APPLICATION_KEY, data, hashlib.sha256).hexdigest()
if calculated_hmac == request.headers.get('X-Gapi-Signature'):
return "OK", 200
# Signatures did not match, raise an exception or don't use the web hook..
Alternatively, we have also implemented the above as a helper method inside our
gapipy
project which can be used as follows,
from flask import request
from gapipy.utils import compute_request_signature
APPLICATION_KEY = 'MY_SECRET_APPLICATION_KEY'
@application.route("/webhook", methods=["POST"])
def webhooks_receiver():
data = request.get_json(force=True)
calculated_hmac = compute_request_signature(APPLICATION_KEY, request.data)
if calculated_hmac == request.headers.get("X-Gapi-Signature"):
return "OK", 200
# Signatures did not match, raise an exception or don't use the webhook
Most languages and frameworks you work with should have the ability to compute an HMAC using the sha256 algorithm. This process is of course optional but can help you sleep better at night in ensuring no unverified requests are making it to your web hook receiver.
Events¶
The POST containing:
event_type
– a string describing the event ({resource}.updates
,{resource}.deleted
).resource
- identifies the resource that triggered the eventcreated
- the datetime of when the event was created. (See Dates & Times)data
- a collection of data, containing the unique reference (id
) and URL (href
) to the specific object of concern.
The event_type
string is for identifying what operation has occurred on a
resource and has a consistent pattern across all resources,
RESOURCE_TYPE.EVENT_NAME
. The resource matches the existing resources
available in the API and the event name can be either updated
,
or deleted
.
We used to provide created
event types, but this has been discontinued
since March 2017. You can safely use the updated
event to capture any
scenario where new, or existing data is relevant to you.
Webhook events contain a list of items since many actions affect multiple resources. For example, adding (CREATE) a departure service to a booking will cause a Webhook event with the following list of items:
departure_services.updated
bookings.updated
(e.g. booking.amount_owing is increased, as well as other fields).invoices.updated
(new invoice for every action that affects invoice amounts)documents.updated
(there is a new document for every invoice)
Cancelling that same departure service would have a similar Webhook event generated, with multiple items.
XML Schema: events.xsd
Note
For security and privacy, the full object is not included in the body.
The id
and href
references allow you to retrieve the resource securely
from the API using your API Key. To secure your endpoint, only retrieve
data from rest.gadventures.com.
Example Event Body¶
[
{
"event_type": "departures_services.updated",
"resource": "departure_services",
"created": "2013-05-17T05:34:38Z",
"data": {
"id": "123",
"href": "|live_url|/departure_services/123"
}
},
{
"event_type": "bookings.updated",
"resource": "bookings",
"created": "2013-05-17T05:34:38Z",
"data": {
"id": "123",
"href": "|live_url|/bookings/123"
}
},
{
"event_type": "invoices.updated",
"resource": "invoices",
"created": "2013-05-17T05:34:38Z",
"data": {
"id": "123",
"href": "|live_url|/invoices/123"
}
},
{
"event_type": "documents.updated",
"resource": "documents",
"created": "2013-05-17T05:34:38Z",
"data": {
"id": "123",
"href": "|live_url|/documents/123"
}
}
]
Example XML Event Body¶
<events>
<event>
<event_type>departure_services.updated</event_type>
<resource>departure_services</resource>
<created>2013-05-17T05:34:38Z</created>
<data>
<id>123</id>
<href>https://rest.gadventures.com/departure_services/123</href>
</data>
</event>
<event>
<event_type>bookings.updated</event_type>
<resource>bookings</resource>
<created>2013-05-17T05:34:38Z</created>
<data>
<id>123</id>
<href>https://rest.gadventures.com/bookings/123</href>
</data>
</event>
<event>
<event_type>invoices.updated</event_type>
<resource>invoices</resource>
<created>2013-05-17T05:34:38Z</created>
<data>
<id>123</id>
<href>https://rest.gadventures.com/invoices/123</href>
</data>
</event>
<event>
<event_type>documents.updated</event_type>
<resource>documents</resource>
<created>2013-05-17T05:34:38Z</created>
<data>
<id>123</id>
<href>https://rest.gadventures.com/documents/123</href>
</data>
</event>
</events>
Best Practices¶
Initial Load & Lifecycle¶
We recommend using Webhooks with the following process to keep a local copy of API data:
Write a script to iterate through the list view of a resource, following all links to other resources.
Store a copy of each relevant resource in a local database, cache, etc…
Use Webhook events as a notification to
create
,update
, ordelete
your local copy of each resource.- Webhook events can also be used to trigger internal processes, for example:
A change in value for a particular resource field may trigger the execution of a piece of code.
Sending an email.
Adding a task to a queue.
Repeat for each relevant resource list view.
Following this best practice has the following benefits:
The script to iterate through all resources only needs to be executed once.
No need for polling of updates to resources, minimizing calls to the API and maximizing internal efficiency.
Real-time updates and synchronization of your website, backend systems, etc…
System Architecture¶
We DON’T rate limit outgoing Webhook POSTs. This means that it is important that your receiver add all incoming requests to a queue to be processed (i.e. make a subsequent API call the the resource/ID identified in the Webhook) according to your own system limits. If you try to process all Webhook events immediately upon receiving them, this could lead to problems.
For example, at certain times of the year we bulk update all of our departure pricing. This bulk operation causes thousands (if not tens of thousands) of Webhook events to be generated in a very short period of time. Trying to process all of these Webhook events in real-time will likely cause problems, leading to your server to return a 500 or 504 error code.
You will also want to have your Webhook queue processing integrated with whatever throttling mechanism you are using to avoid hitting our Caching. This is another good reason for using a queue to process Webhook events.
Disabling Webhooks¶
If we receive 500 non 200 responses to outgoing Webhook events in a one hour time frame, we will send a warning email to the email address associated with that API account. If we receive an additional 250 non 200 responses to outgoing Webhook events in that same one hour time frame, we will disable the Webhooks for that API application. An automated email will be sent to the email address associated with that API account, indicating that we have disabled the Webhook for that application. You will need to email support to have the Webhook re-enabled.
Testing Webhooks¶
The best ways to test webhooks:
Webhook Playground¶
Using the Webhooks Playground is the best way to first test your server’s webhook listener to ensure you are receiving the event and correctly processing the data contained within the body of the webhook. These tests only contain a single item in the webhook body list of events.
Writing to resources¶
Testing of booking-related resources can be done via WRITE actions using your test api key. A CREATE or UPDATE action on a resource will cause a Webhook to be fired with events for one or more resources. This is a good way to test making bookings, cancelling services, updating customers, etc…