# API Webhooks 3.0 Implementation ### Introduction Webhooks are simple HTTP requests that are sent out when a given event occurs. You can define a single or separate handlers (URLs) for each event, and when this event handler receives a webhooks request, it should return an HTTP 200 response accordingly. Webhooks 3.0 is our latest version of Webhooks. Compared to our previous versions, this version has more endpoints and more detailed notifications for products & product types. **Note:** Webhooks are just an additional optional implementation for the API. It is not intended to replace API requests, especially for vouchers. For vouchers, it is recommended to request the BMG Vouchers API periodically to check whether a voucher is ready for download instead of fully relying on Webhooks notifications. ### Webhooks Payload Detail - Webhooks are sent as `POST` HTTP/1.1 requests with a JSON payload. - The payload can vary depending on the event but will always contain `type`, `timestamp`, and `signature`. - Webhooks expect a `200 OK` HTTP response as a confirmation, meaning the event has been acknowledged by your application. ### Webhooks Acknowledgement - The webhooks handler from your server is expected to return an HTTP `200 OK` response as event acknowledgement. - When a webhook receives a `200` response from your webhooks handler, it is marked as acknowledged and will not be sent again. - If our webhooks get any other HTTP response or no response at all after waiting for more than 20 seconds, the webhook delivery is marked as failed, and there will be a retry. ### Number of Retries The total number of retries is 10 times. - **1st retry:** 1 minute after the original webhook. - **2nd retry:** 2 minutes after the 1st retry. - **Subsequent retries:** Intervals will be 1, 2, 4, 8, 16, 32, 64 minutes on the same day. - After the 7th retry, subsequent retries will be every 24 hours for the next 3 days. - If, after subsequent retries for the next 3 days, it still fails, the Webhook for this event will be auto-deactivated. You will have to resolve the issues and then log in to the Agents Marketplace to re-enable it. ### Webhook Events | Name | Description | | --- | --- | | `product_available` | Fired when a new Product is available for booking. | | `product_updated` | Fired when existing fields' data in the Product API endpoint changes. | | `product_type_available` | Fired when a new Product Type is online. | | `product_type_not_available` | Fired when an existing active Product Type is offline. | | `product_type_content_updated` | Fired when existing content fields' data in the Product Type API endpoint changes. | | `product_type_availability_update` | Fired when a product type availability date has been updated. | | `product_type_pricing_updated` | Fired when a product type base pricing has been updated. Note: The price of a Product type may be different per date; please use the Product types pricing API endpoint to get the latest pricing based on dates. | | `booking_status_updated` | Fired whenever the booking's status changes. | | `booking_data_updated` | Fired whenever there is a change in the booking's data, excluding status. | | `booking_tickets_updated` | Fired when vouchers for an approved booking have been generated or updated and are ready to be downloaded. | ### Webhooks Security To ensure the origin of the webhook call, you need to use payload signing. In the Agents Marketplace webhooks configuration page, you can (re)generate your hash secret key, which will be used to create an HMAC for the webhook call. The signature is a `sha256` HMAC of the payload in JSON format with your hash secret key. It is based on the values of two fields, `type` and `timestamp`, concatenated using `|`. **Note:** A webhook will only be activated when the hash secret key has been generated. Please make sure you have generated the hash secret key. **Example of checking the signature in PHP:** ```php $json = json_decode($request->getBody(), true); $signature = $json["signature"]; $payload = $json; $signature === hash_hmac('sha256', $payload['type'].'|'.$payload['timestamp'], 'MY_WEBHOOK_SECRET_KEY'); ``` ### Steps in Configuring Webhooks 1. Create a webhook event handler on your server. 2. The main task for this webhook handler is to receive the payload from our Webhooks server, save it as a job in your server queue, and then return an HTTP `200` response. You must ensure it can return a response within 20 seconds. 3. Create a job in your server to loop through the server queue and process each of the jobs accordingly. 4. Log in to the **Agents MarketPlace**. 5. Click on **API** and then the **Webhooks 3** tab. 6. Generate a webhook hash secret key by clicking on the **Generate** button. 7. Input a valid URL of your event handler for each of the webhook events. 8. Update the webhooks event handler in your server to use this new secret key. 9. Start testing your webhooks event handler. ### Webhooks Event JSON Payload Samples #### `product_available` ```json { "type": "product_available", "data": { "uuid": "862a4a30-533f-57dc-94d0-dab9d59bacc5", "updatedAt": "2019-01-18 15:30:35", "title": "Sentosa Day Fun Pass", "titleTranslated": "Sentosa Day Fun Pass", "description": "Discover endless fun and surprises at Sentosa...", "descriptionTranslated": "Discover endless fun and surprises at Sentosa...", "highlights": "Experience the favourite attractions and activities in Sentosa...", "highlightsTranslated": "Experience the favourite attractions and activities in Sentosa...", "additionalInfo": "Notes:\n- The price of Fun Passes excludes island admission...", "additionalInfoTranslated": "Notes:\n- The price of Fun Passes excludes island admission...", "covid19Measures": "Information on Covid-19 notice of Safety Standards...", "covid19MeasuresTranslated": "Information on Covid-19 notice of Safety Standards...", "priceIncludes": "Admission fee", "priceIncludesTranslated": "Admission fee", "priceExcludes": "Transfer services\nPersonal expenses\nTips", "priceExcludesTranslated": "Transfer services\nPersonal expenses\nTips", "validFrom": null, "validThrough": null, "itinerary": "", "itineraryTranslated": "", "warnings": "- In cases of extreme weather conditions...", "warningsTranslated": "- In cases of extreme weather conditions...", "safety": null, "safetyTranslated": null, "latitude": "1.2485520", "longitude": "103.8304437", "minPax": 1, "maxPax": 60, "basePrice": 45.58, "currency": { "code": "SGD", "symbol": "S$", "uuid": "cd15153e-dfd1-5039-8aa3-115bec58e86e" }, "isFlatPaxPrice": true, "reviewCount": 1, "reviewAverageScore": 4, "typeName": "Attraction", "typeUuid": "d3c54653-dd05-598f-b193-f6683d1064ab", "photosUrl": "https://s3.amazonaws.com/playground.bemyguest.com.sg", "businessHoursFrom": "09:00", "businessHoursTo": "22:00", "averageDelivery": 567, "hotelPickup": false, "airportPickup": false, "hasOptions": false, "allProductTypesHaveOptions": false, "photos": [ { "caption": null, "uuid": "f6f289e9-3545-51d4-9530-c4b5dfe1f5d8", "paths": { "original": "/images/content/original/f6f289e9-3545-51d4-9530-c4b5dfe1f5d8.jpg", "75x50": "/images/content/75x50/f6f289e9-3545-51d4-9530-c4b5dfe1f5d8.jpg", "175x112": "/images/content/175x112/f6f289e9-3545-51d4-9530-c4b5dfe1f5d8.jpg", "680x325": "/images/content/680x325/f6f289e9-3545-51d4-9530-c4b5dfe1f5d8.jpg", "1280x720": null, "1920x1080": null, "2048x1536": null } } ], "categories": [ { "name": "Themeparks", "uuid": "9384f408-a941-58ba-b958-4a814c23d1bc" } ], "locations": [ { "city": "Sentosa", "cityUuid": "29ad1bd7-2000-5321-aff8-f71a6464142b", "state": "Sentosa", "stateUuid": "ab7d9ac9-036d-11e5-a2a9-d07e352b4840", "country": "Singapore", "countryUuid": "885e90a3-a83d-56d3-b5e8-040b4017c825" } ], "guideLanguages": [ { "name": "english", "uuid": "790395c0-23b6-5dd9-8a6b-ca61b95dcfd4" } ], "audioHeadsetLanguages": [ { "name": "english", "uuid": "790395c0-23b6-5dd9-8a6b-ca61b95dcfd4" } ], "writtenLanguages": [ { "name": "english", "uuid": "790395c0-23b6-5dd9-8a6b-ca61b95dcfd4" } ], "links": [ { "method": "GET", "rel": "self", "href": "https://api.demo.bemyguest.com.sg/v2/products/862a4a30-533f-57dc-94d0-dab9d59bacc5" }, { "method": "GET", "rel": "productTypes", "href": "https://api.demo.bemyguest.com.sg/v2/products/862a4a30-533f-57dc-94d0-dab9d59bacc5/product-types" } ], "translationLanguages": [ { "code": "ZH-HANS", "name": "chinese_simplified", "uuid": "d182fbd9-4520-5c66-a513-94fcd3d46d9b" } ] }, "signature": "gdfhgy786g78dfg7d7f8gdfghgfhgk6786786868", "timestamp": "2020-11-27T18:00:27+08:00" } #### `product_updated` ```json { "type": "product_updated", "data": { "uuid": "862a4a30-533f-57dc-94d0-dab9d59bacc5", "updatedAt": "2019-01-18 15:30:35", "title": "Sentosa Day Fun Pass", "titleTranslated": "Sentosa Day Fun Pass", "description": "Discover endless fun and surprises at Sentosa...", "descriptionTranslated": "Discover endless fun and surprises at Sentosa...", "highlights": "Experience the favourite attractions and activities in Sentosa...", "highlightsTranslated": "Experience the favourite attractions and activities in Sentosa...", "additionalInfo": "Notes:\n- The price of Fun Passes excludes island admission...", "additionalInfoTranslated": "Notes:\n- The price of Fun Passes excludes island admission...", "covid19Measures": "Information on Covid-19 notice of Safety Standards...", "covid19MeasuresTranslated": "Information on Covid-19 notice of Safety Standards...", "priceIncludes": "Admission fee", "priceIncludesTranslated": "Admission fee", "priceExcludes": "Transfer services\nPersonal expenses\nTips", "priceExcludesTranslated": "Transfer services\nPersonal expenses\nTips", "validFrom": null, "validThrough": null, "itinerary": "", "itineraryTranslated": "", "warnings": "- In cases of extreme weather conditions...", "warningsTranslated": "- In cases of extreme weather conditions...", "safety": null, "safetyTranslated": null, "latitude": "1.2485520", "longitude": "103.8304437", "minPax": 1, "maxPax": 60, "basePrice": 45.58, "currency": { "code": "SGD", "symbol": "S$", "uuid": "cd15153e-dfd1-5039-8aa3-115bec58e86e" }, "isFlatPaxPrice": true, "reviewCount": 1, "reviewAverageScore": 4, "typeName": "Attraction", "typeUuid": "d3c54653-dd05-598f-b193-f6683d1064ab", "photosUrl": "https://s3.amazonaws.com/playground.bemyguest.com.sg", "businessHoursFrom": "09:00", "businessHoursTo": "22:00", "averageDelivery": 567, "hotelPickup": false, "airportPickup": false, "hasOptions": false, "allProductTypesHaveOptions": false, "photos": [ { "caption": null, "uuid": "f6f289e9-3545-51d4-9530-c4b5dfe1f5d8", "paths": { "original": "/images/content/original/f6f289e9-3545-51d4-9530-c4b5dfe1f5d8.jpg", "75x50": "/images/content/75x50/f6f289e9-3545-51d4-9530-c4b5dfe1f5d8.jpg", "175x112": "/images/content/175x112/f6f289e9-3545-51d4-9530-c4b5dfe1f5d8.jpg", "680x325": "/images/content/680x325/f6f289e9-3545-51d4-9530-c4b5dfe1f5d8.jpg", "1280x720": null, "1920x1080": null, "2048x1536": null } } ], "categories": [ { "name": "Themeparks", "uuid": "9384f408-a941-58ba-b958-4a814c23d1bc" } ], "locations": [ { "city": "Sentosa", "cityUuid": "29ad1bd7-2000-5321-aff8-f71a6464142b", "state": "Sentosa", "stateUuid": "ab7d9ac9-036d-11e5-a2a9-d07e352b4840", "country": "Singapore", "countryUuid": "885e90a3-a83d-56d3-b5e8-040b4017c825" } ], "guideLanguages": [ { "name": "english", "uuid": "790395c0-23b6-5dd9-8a6b-ca61b95dcfd4" } ], "audioHeadsetLanguages": [ { "name": "english", "uuid": "790395c0-23b6-5dd9-8a6b-ca61b95dcfd4" } ], "writtenLanguages": [ { "name": "english", "uuid": "790395c0-23b6-5dd9-8a6b-ca61b95dcfd4" } ], "links": [ { "method": "GET", "rel": "self", "href": "https://api.demo.bemyguest.com.sg/v2/products/862a4a30-533f-57dc-94d0-dab9d59bacc5" }, { "method": "GET", "rel": "productTypes", "href": "https://api.demo.bemyguest.com.sg/v2/products/862a4a30-533f-57dc-94d0-dab9d59bacc5/product-types" } ], "translationLanguages": [ { "code": "ZH-HANS", "name": "chinese_simplified", "uuid": "d182fbd9-4520-5c66-a513-94fcd3d46d9b" } ] }, "updatedFields": [ "photos", "warnings" ], "signature": "gdfhgy786g78dfg7d7f8gdfghgfhgk6786786868", "timestamp": "2020-11-27T18:00:27+08:00" } #### `product_type_available` ```json { "type": "product_type_available", "productUuid": "862a4a30-533f-57dc-94d0-dab9d59bacc5", "productTypeUuid": "b92e76dc-6953-40dc-b0e6-8bb600f8e5b3", "endpoint": "https://api.demo.bemyguest.com.sg/v2/product-types/b92e76dc-6953-40dc-b0e6-8bb600f8e5b3/price-lists", "timestamp": "2019-08-29T13:45:52+08:00", "signature": "6c2e4efed3f902612ef1b9367221a7b3002c79d6714c5a183cafc4eda173ede2" } ``` #### `product_type_not_available` ```json { "type": "product_type_not_available", "productUuid": "862a4a30-533f-57dc-94d0-dab9d59bacc5", "productTypeUuid": "e96a52d5-9488-5891-bf5a-ce60caec8d1f", "signature": "gdfhgy786g78dfg7d7f8gdfghgfhgk6786786868", "timestamp": "2020-11-27T18:00:27+08:00" } ``` #### `product_type_content_updated` ```json { "type": "product_type_content_updated", "data": { "uuid": "e96a52d5-9488-5891-bf5a-ce60caec8d1f", "title": "Fun Pass Play 3", "titleTranslated": "Fun Pass Play 3", "description": "Choose Fun Pass Play 3 to play 3 out of 18 attractions", "descriptionTranslated": "Choose Fun Pass Play 3 to play 3 out of 18 attractions", "durationDays": 0, "durationHours": 12, "durationMinutes": 0, "daysInAdvance": null, "cutOffTime": null, "firstAvailabilityDate": "2019-07-22", "isNonRefundable": true, "allowAdults": true, "minPax": 1, "maxPax": 60, "minAdultAge": 13, "maxAdultAge": 100, "hasChildPrice": true, "allowChildren": true, "minChildren": 0, "maxChildren": 20, "minChildAge": 3, "maxChildAge": 12, "allowSeniors": false, "minSeniors": null, "maxSeniors": null, "minSeniorAge": null, "maxSeniorAge": null, "allowInfant": false, "minInfantAge": null, "maxInfantAge": null, "maxGroup": null, "minGroup": null, "instantConfirmation": true, "nonInstantVoucher": true, "directAdmission": false, "voucherUse": "You will receive a voucher by email...", "voucherUseTranslated": "You will receive a voucher by email...", "voucherRedemptionAddress": "Sentosa Ticketing Counters...", "voucherRedemptionAddressTranslated": "Sentosa Ticketing Counters...", "voucherRequiresPrinting": false, "meetingTime": null, "meetingAddress": null, "meetingLocation": "Make your own way\nto Sentosa Ticketing Counters", "meetingLocationTranslated": "Make your own way\nto Sentosa Ticketing Counters", "cancellationPolicies": [], "recommendedMarkup": 6.67, "childRecommendedMarkup": null, "seniorRecommendedMarkup": null, "adultParityPrice": null, "childParityPrice": null, "seniorParityPrice": null, "adultGateRatePrice": 0, "childGateRatePrice": 0, "seniorGateRatePrice": null, "validity": { "type": "after_issue_date", "days": 90, "date": null, "hasBatchValidityDate": false }, "timeslots": [], "options": { "perBooking": [], "perPax": [] }, "hasOptions": false, "hasFileUploadOptions": false, "hasPriceOptions": false, "links": [ { "method": "GET", "rel": "self", "href": "https://api.demo.bemyguest.com.sg/v2/product-types/e96a52d5-9488-5891-bf5a-ce60caec8d1f" }, { "method": "GET", "rel": "product", "href": "https://api.demo.bemyguest.com.sg/v2/products/862a4a30-533f-57dc-94d0-dab9d59bacc5" }, { "method": "GET", "rel": "priceLists", "href": "https://api.demo.bemyguest.com.sg/v2/product-types/e96a52d5-9488-5891-bf5a-ce60caec8d1f/price-lists" } ] }, "updatedFields": [ "minPax", "minChildren" ], "signature": "gdfhgy786g78dfg7d7f8gdfghgfhgk6786786868", "timestamp": "2020-11-27T18:00:27+08:00" } #### `product_type_availability_updated` ```json { "productTypeUuid": "608a63b6-bdf7-4413-942c-7ad827d6f037", "dates": { "blackouts": { "0": "2019-01-22", "1": "2019-01-23", "2": "2019-03-06", "3": "2019-03-07" } }, "type": "product_type_availability_updated", "timestamp": "2020-01-28T11:48:54+08:00", "signature": "056b7bc898cee89d38b92df71c0f90e3ea76dc172e1ebf3008d7fb2aff5c2365" } ``` #### `product_type_pricing_updated` ```json { "type": "product_type_pricing_updated", "productTypeUuid": "e96a52d5-9488-5891-bf5a-ce60caec8d1f", "endpoint": "https://api.demo.bemyguest.com.sg/v2/product-types/e96a52d5-9488-5891-bf5a-ce60caec8d1f/price-lists", "signature": "gdfhgy786g78dfg7d7f8gdfghgfhgk6786786868", "timestamp": "2020-11-27T18:00:27+08:00" } ``` #### `booking_status_updated` ```json { "type": "booking_status_updated", "bookingUuid": "aae5cbca-c065-31c3-ab49-21fc53fade89", "previousStatus": "waiting", "currentStatus": "approved", "signature": "gdfhgy786g78dfg7d7f8gdfghgfhgk6786786868", "timestamp": "2020-11-27T18:00:27+08:00" } ``` #### `booking_data_updated` ```json { "type": "booking_data_updated", "bookingUuid": "aae5cbca-c065-31c3-ab49-21fc53fade89", "data": { "uuid": "aae5cbca-c065-31c3-ab49-21fc53fade89", "code": "2D5AD6G", "partnerReference": "Test-12345566", "status": "approved", "productTypeTitle": "National Orchid Garden E-ticket", "productTypeTitleTranslated": "National Orchid Garden E-ticket", "productTypeUuid": "f57358a6-4854-5d36-8eaf-0cecc053af59", "currencyCode": "SGD", "currencyUuid": "cd15153e-dfd1-5039-8aa3-115bec58e86e", "totalAmount": 2.84, "amountBreakdown": [ { "name": "adult", "quantity": 1, "price": "2.84" } ], "arrivalDate": "2019-08-12", "createdAt": "2019-08-11 14:18:52", "updatedAt": "2019-08-11 14:20:06", "salutation": "Mr.", "firstName": "Tester", "lastName": null, "email": "tester@bemyguest.com.sg", "phone": "+6512345678", "adults": 1, "children": 0, "seniors": null, "options": [], "completedAt": "2019-07-21 14:20:06", "cancellationRequestAt": null, "cancellationRequestStatus": "none", "cancellationStatus": null, "refundDate": null, "refundAmount": null, "refundTransaction": null, "links": [ { "method": "GET", "rel": "self", "href": "https://api.demo.bemyguest.com.sg/v2/bookings/aae5cbca-c065-31c3-ab49-21fc53fade89" }, { "method": "GET", "rel": "vouchers", "href": "https://api.demo.bemyguest.com.sg/v2/bookings/aae5cbca-c065-31c3-ab49-21fc53fade89/vouchers" }, { "method": "GET", "rel": "productType", "href": "https://api.demo.bemyguest.com.sg/v2/product-types/f57358a6-4854-5d36-8eaf-0cecc053af59" }, { "method": "GET", "rel": "product", "href": "https://api.demo.bemyguest.com.sg/v2/products/6aca0761-c17d-56a4-87a2-adddcb987b7c" } ] }, "updatedFields": [ "arrivalDate", "phone" ], "signature": "gdfhgy786g78dfg7d7f8gdfghgfhgk6786786868", "timestamp": "2020-11-27T18:00:27+08:00" } #### `booking_tickets_updated` ```json { "type": "booking_tickets_updated", "bookingUuid": "aae5cbca-c065-31c3-ab49-21fc53fade89", "data": [ { "uuid": "45dffgg-g6h8-3f6j-6789-93b1dc90ba72", "generatedAt": "2019-07-22 14:19:57", "downloadedAt": null, "links": [ { "method": "GET", "rel": "download", "href": "https://api.demo.bemyguest.com.sg/v2/bookings/aae5cbca-c065-31c3-ab49-21fc53fade89/download-voucher/9f5a925e-70ca5678-5566-f11f25d6c5fb" }, { "method": "GET", "rel": "booking", "href": "https://api.demo.bemyguest.com.sg/v2/bookings/aae5cbca-c065-31c3-ab49-21fc53fade89" } ] } ], "signature": "gdfhgy786g78dfg7d7f8gdfghgfhgk6786786868", "timestamp": "2020-11-27T18:00:27+08:00" } ``` #### Webhooks Event JSON Response Sample ```json { "code": 200, "message": "success", "timestamp": "2020-11-27T18:00:27+08:00" } ```