Using Webhooks
While most Grubhub API functions require your POS integration to actively call our endpoints, some functions can be performed more passively through webhooks. A webhook is a URI you maintain that receives requests from our system. Like our endpoints, the webhook you designate has to be able to authenticate our requests and parse a JSON object.
We can push the following to a webhook you maintain:
- Order status changes
- Driver updates
- Full menu ingestion status
- Asynchronous bulk menu update status
- Full menu update media ingestion status
Great! Where do I sign up?
To use webhooks within your POS integration, you'll need to contact your Grubhub representative. We need to test the webhook URL to make sure that it works with the payload that we send. If that test succeeds, we configure our settings to include the URL and associate it with your account.
We do not allow partners to set their own webhook URLs without contacting us first, as that could have us blindly calling an arbitrary URI.
But before we can approve your webhook URL, you'll need to create it and configure it to accept our authorization and process the JSON payload that we send.
Authorization
To verify that the payloads your webhooks receive actually come from us, we require that you implement one of three authentication schemes:
- basic authorization
- HMAC
- JWT
Basic authentication
This is just a simple user/password combination. When you sign up with us and decide to use basic auth, you can also pick a user name and password that will grant access to your webhook. The Grubhub call to the webhook will include this information in the header. To ensure that this header information is not intercepted by third parties, we send all requests via SSL.
HMAC
HMAC constructs an information-rich header that contains information about the sender, while providing security to ensure that the request has not been forged or tampered with in transit. It is the authentication scheme we recommend for most calls to the Grubhub API.
JWT
JSON web tokens (JWT) is a simple, stateless authentication scheme. It encodes the payload of the webhook event signed by a shared secret. In this way, you can verify that the contents of the webhook payload came from Grubhub and were not tampered with in transit.
A JWT contains three parts: a header, a payload, and a signature. The header lists the algorithm for the signature and the type, which is JWT. For example:
{
"alg": "HS256",
"typ": "JWT"
}
The payload is the same as the webhook payload. Because this is a webhook emission and we don't need to persist any claims, our JWT payload matches the webhook payload. You can use the included payload and signature to verify that no changes occurred in transit.
The signature hashes the header, payload, and a shared secret using the algorithm listed in the header.
These three parts are individually base64 encoded and sent in the webhook header, separated by dots (.), as shown below:
Authorization: Bearer [header].[payload].[signature]
For example:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZWxpdmVyeSI6eyJpZCI6Inh4eHh4eHh4eHh4LXh4eHh4eHhfeHgiLCJhY2NvdW50X2lkIjoieHh4eHh4eHh4eHh4eHh4eHh4eHh4eHgiLCJjb25maXJtYXRpb25fY29kZSI6Inh4eHh4eCIsInRyaXBfaWQiOm51bGwsIm5hbWUiOiJQb3MgZGVsaXZlcnkiLCJwaWNrdXAiOnsiaWQiOm51bGwsIm5hbWUiOiJwaWNrdXAiLCJhZGRyZXNzIjp7InN0cmVldF9hZGRyZXNzMSI6IlRlc3QgU3QiLCJzdHJlZXRfYWRkcmVzczIiOm51bGwsImNpdHkiOiJDaGljYWdvIiwic3RhdGUiOiJJTCIsInBvc3RhbF9jb2RlIjoiMDAwMDAiLCJjb3VudHJ5IjoiVVNBIn0sInBob25lIjoiMDAwLTAwMC0wMDAwIiwibm90ZXMiOm51bGwsImdlbyI6eyJsYXQiOjEwLCJsbmciOi0xMH19LCJkcm9wb2ZmIjp7ImlkIjpudWxsLCJuYW1lIjoicGlja3VwIiwiYWRkcmVzcyI6eyJzdHJlZXRfYWRkcmVzczEiOiJUZXN0IFN0Iiwic3RyZWV0X2FkZHJlc3MyIjpudWxsLCJjaXR5IjoiQ2hpY2FnbyIsInN0YXRlIjoiSUwiLCJwb3N0YWxfY29kZSI6IjAwMDAwIiwiY291bnRyeSI6IlVTQSJ9LCJwaG9uZSI6IjAwMC0wMDAtMDAwMCIsIm5vdGVzIjpudWxsLCJnZW8iOnsibGF0IjoxMCwibG5nIjotMTB9fSwiZmxhZ3MiOm51bGwsImNvbnRlbnRzIjpbeyJkZXNjcmlwdGlvbiI6Iml0ZW0iLCJzaXplIjpudWxsLCJ0YWdzIjpudWxsLCJxdWFudGl0eSI6MX1dLCJzdGF0dXMiOiJDT01NSVRURUQiLCJjb3VyaWVyIjpudWxsLCJjcmVhdGVkIjoiMjAwMC0wMC0wMFQwMDowMDowMC4wMDBaIiwidGltZXMiOnsicGlja3VwIjp7InR5cGUiOiJlc3RpbWF0ZSIsInRpbWVzdGFtcCI6IjIwMDAtMDAtMDBUMDA6MDA6MDAuMDAwWiJ9LCJkcm9wb2ZmIjp7InR5cGUiOiJlc3RpbWF0ZSIsInRpbWVzdGFtcCI6IjIwMDAtMDAtMDBUMDA6MDA6MDAuMDAwWiJ9fSwiY29tbWl0dGVkX3RpbWVzIjpudWxsLCJjb3N0Ijp7ImFtb3VudCI6MTAwLCJjdXJyZW5jeSI6IlVTRCJ9LCJjb2xsZWN0ZWRfdGlwIjp7ImFtb3VudCI6MCwiY3VycmVuY3kiOiJVU0QifSwidG90YWwiOm51bGwsInByZWZlcmVuY2VzIjp7InBpY2t1cF90aW1lIjpudWxsLCJkZWxpdmVyeV90aW1lIjpudWxsfSwibWV0YWRhdGEiOnsiZ3J1Ymh1Yl9jdXN0X2lkIjoiMDAwMDAwMDAiLCJncnViaHViX2NvbmZpcm1hdGlvbl9jb2RlIjoieHh4eHh4IiwiZ3J1Ymh1Yl9oYXNfYWxjb2hvbCI6ImZhbHNlIiwiZ3J1Ymh1Yl9uZWVkc19hdHRlbnRpb24iOiJmYWxzZSIsImdydWJodWJfaXNfbGVnYWN5X2NyZWF0ZWRfb3JkZXIiOiJmYWxzZSIsImdydWJodWJfb3JkZXJfdHlwZSI6IkFTQVAiLCJncnViaHViX25vdF9mb3JfYmlrZXIiOiJmYWxzZSJ9LCJhbHRlcm5hdGVfbmFtZSI6bnVsbCwiYWx0ZXJuYXRlX3Bob25lIjpudWxsfSwidGltZV9zdGFtcCI6IjIwMDAtMDAtMDBUMDA6MDA6MDAuMDAwWiIsImRlbGl2ZXJ5X3N0YXR1cyI6IkNPTU1JVFRFRCJ9.fyjzsUwSeBInPM3yarkE5mwDHOnzelmATtWYywCPBOI
Whichever method you choose, you'll need to let your account representative know, so that we can configure the requests on our end to authenticate properly with your webhooks.
Using Webhooks in your POS Integration
Once you have authentication in place and everything configured with Grubhub, what can you expect from a webhook in your implementation? When do they emit a payload and what does that payload look like? To answer this, we need to cover each webhook individually.
Order Status
Instead of polling for order status changes, you can receive them when they change. The order status webhook emits a payload anytime an order changes its status, say from RESTAURANT_CONFIRMABLE to CONFIRMED.
Order changes include new orders, so you can use this webhook to process those as well.
This webhook emits an Order object, which looks like this:
{
"uuid":"UKBI2dGvRCamk2lEeCLiAw",
"order_number":"ORD-16-1486752039496",
"is_test":false,
"status":"RESTAURANT_CONFIRMABLE",
"status_history":
[
{
"status":"RESTAURANT_CONFIRMABLE",
"timestamp":"2017-03-16T15:30:39.801Z",
"update_source":"GRUBHUB",
"reason":null
}
],
"updated_at":"2017-03-16T15:30:39.801Z",
"merchant_id":"630989",
"fulfillment_info":
{
"delivery_info":null,
"pickup_info":
{
"name":"Mrs Person",
"contact_info":
{
"phone":"",
"name":"Mrs Person"
},
"instructions":"",
"estimated_pickup_time":null,
"is_green_indicated":false
}
},
"brand":"GRUBHUB",
"time_placed":"2017-03-16T15:30:39.801Z",
"confirmation_code":null,
"when_for":"2017-03-16T15:30:39.801Z",
"restaurant_timezone_id":"UTC",
"payments":
{
"payments":
[
{
"payment_type":"CASH",
"amount":100
}
],
"total":0,
"adjusted_total":100
},
"charges":
{
"fees":
{
"total":100,
"delivery":100
},
"taxes":
{
"total":300,
"sales":100,
"delivery":100,
"restaurant":100
},
"tip":
{
"amount":100,
"type":"INCLUDE_IN_BILL"
},
"diner_grand_total":2104,
"grand_total":2104,
"line_groups":
[
{
"label":null,
"lines":
[
{
"description":null,
"name":"French Fries",
"special_instructions":null,
"line_options":[],
"price":0,
"quantity":1,
"id":null,
"menu_item_id":"123",
"menu_item_uuid":"AAAAAAAAAHsAAAAAAAAAew",
"diner_total":0,
"total":299,
"item_type":"Side",
"variation_id":null,
"tags":null,
"external_id":"external_123"
}
]
}
],
"coupons":[]
},
"catering":null
}
This is the full order information object. Each new status change will be added to the status_history object, so you can read the progress that this order has made. For new orders, you can gather all the necessary information to send to the kitchen and prepare the meal.
Just-in-time (JIT) preparation
For quick-service restaurants (QSRs), you can enable a JIT webhook, which will send an order to your kitchen only once the driver coming to pick it up is within the time-based estimated distance from the restaurant. Like the order status webhook, this webhook will emit an Order object.
While you can determine JIT events from the delivery webhook shown below, this webhook fires on any of these events:
- The recipient (driver) remaining transit time is estimated to be within the restaurant prep time.
- The driver marks that they have arrived at the restaurant.
- The estimated pickup time is within the prep time for the restaurant.
Delivery Updates
If you intend for your POS integration to support restaurants that use Grubhub Delivery (GHD), you may want to enable the delivery webhook. This webhook emits a Delivery object whenever that object changes, say when a driver is assigned or they update their location.
That object looks like this:
{
"delivery": {
"id": "xxx-xxxxxxxxxxxxxx",
"account_id": "xxxxxxxxxxxxxxxx",
"confirmation_code": "HVUYBJH",
"trip_id": "UvYO6VyVEeiVadmHZVMREg",
"name": "Grubhub order 76884765873",
"pickup": {
"name": "Good Food, Inc.",
"address": {
"street_address1": "123 Main Ave",
"city": "Anytown",
"state": "NY",
"postal_code": "11111",
"country": "USA"
},
"phone": "4045551212",
"notes": "",
"geo": {
"lat": 37.65947723,
"lng": -81.43487549
}
},
"dropoff": {
"name": "Steve McSteverson",
"address": {
"street_address1": "2940 Secondary Ln",
"street_address2": "",
"city": "Anytown",
"state": "NY",
"postal_code": "11111",
"country": "US"
},
"phone": "4045552121",
"notes": "",
"geo": {
"lat": 37.67552947,
"lng": -81.40675355
}
},
"contents": [{
"description": "Slider",
"quantity": 6
}, {
"description": "Chocolate Chip Cookie",
"quantity": 1
}, {
"description": "Chocolate Chip Cookie",
"quantity": 1
}],
"status": "DELIVERED",
"courier": {
"id": "X-xxxxxxxxxxxx",
"name": "Jack Burton",
"vehicle": {
"type": "car",
"description": ""
},
"phone": "4045552323",
"photo_url": "https://s3.amazonaws.com/gh-prod-drivers-data/drivers/xxxxx/avatar.png",
"geo": {
"lat": 37.67577137428678,
"lng": -81.40696178943328
}
},
"created": "2018-05-21T00:42:03.300Z",
"pickup_ready": "2018-05-21T00:32:09.492Z",
"pickup_feasibility_time": "2018-05-21T01:12:39.535Z",
"times": {
"pickup_arrival": {
"type": "actual",
"timestamp": "2018-05-21T02:05:51.993Z"
},
"pickup": {
"type": "actual",
"timestamp": "2018-05-21T02:10:58.458Z"
},
"dropoff_arrival": {
"type": "actual",
"timestamp": "2018-05-21T02:38:10.913Z"
},
"dropoff": {
"type": "actual",
"timestamp": "2018-05-21T02:38:12.020Z"
},
"geofence_pickup_arrival": {
"type": "actual",
"timestamp": "2018-05-21T02:05:52.100Z"
}
},
"committed_times": {
"pickup_arrival": {
"type": "estimate",
"timestamp": "2018-05-21T01:12:39.535Z"
},
"pickup": {
"type": "estimate",
"timestamp": "2018-05-21T01:15:39.535Z"
},
"dropoff_arrival": {
"type": "estimate",
"timestamp": "2018-05-21T01:26:46.535Z"
},
"dropoff": {
"type": "estimate",
"timestamp": "2018-05-21T01:28:46.535Z"
}
},
"cost": {
"amount": 696,
"currency": "USD"
},
"collected_tip": {
"amount": 200,
"currency": "USD"
},
"total": {
"amount": 1822,
"currency": "USD"
},
"preferences": {
"pickup_time": "2018-05-21T01:15:39.535Z",
"delivery_time": "2018-05-21T01:23:41.672Z"
},
"metadata": {
"grubhub_cust_id": "735541",
"grubhub_diner_uuid": "f3e00ff0-5c8a-11e8-ad7c-810ac177323e",
"grubhub_order_uuid": "bbd2cef4-5c89-11e8-9ff1-73136060c4b4",
"grubhub_order_number": "321404324050800",
"grubhub_requested_fulfillment_at": "2018-05-21T01:23:41.672Z",
"grubhub_brand": "GRUBHUB",
"grubhub_contact_email": "grubhub987654321@gmail.com",
"grubhub_future_check_v2": "true",
"grubhub_order_type": "ANTICIPATED",
"grubhub_confirmation_code": "LNQDMB",
"grubhub_has_alcohol": "false",
"grubhub_not_for_biker": "false",
"grubhub_needs_attention": "false",
"grubhub_quote.currency": "USD",
"grubhub_quote.amount": "398",
"grubhub_quote.id": "1e7d67c1-56b2-41f6-91a8-2d51f933de5f",
"grubhub_quote.reasons": "BASE_DELIVERY_RATE, BASE_DISTANCE_RATE",
"grubhub_quote.breakdown": "[{\"id\":\"uo57eAfgR3mF3fVaf9_UPQ\",\"cost\":{\"amount\":300,\"currency\":\"USD\"},\"reason\":\"BASE_DELIVERY_RATE\",\"type\":\"DELIVERY\"},{\"id\":\"uo57eAfgR3mF3fVaf9_UPQ\",\"cost\":{\"amount\":50,\"currency\":\"USD\"},\"reason\":\"BASE_DISTANCE_RATE\",\"type\":\"DISTANCE\"}]"
},
"catering_order": false,
"ticket_id": null,
"update_time": "AUFOYFygEeil4nOupoN1xg"
},
"time_stamp": "2018-05-21T02:38:42.326Z",
"delivery_status": "DELIVERED"
}
Note the field courier, which is null in the above object. This field can be useful to watch in order to know when a driver has been assigned and then identify the delivery driver coming to pickup an order.
For those restaurants using just-in-time (JIT) preparation, the webhook will fire an event when either times.pickup_arrival goes from estimated to actual or times.geofence_pickup_arrival is assigned a value. That means the driver has come close to the pickup location (currently 150 meters, but may be configurable in the future), so a quick-service restaurant can assemble the order and provide the customer with the freshest possible food.
Menu Ingestion Job Statuses
You have two options for menu ingestion status, you can poll endpoint or use a webhook.
If you are polling the endpoint
1. After Menu ingestion - POST /pos/v1/menu/ingestion
2. A job Id is returned. Take the job id and make a call to get the menu job status
{
"job_id": "682e4214-4d53-4892-a4a4-788b91c8cbc1"
}
The menu ingestion webhook, works similarly, when there is an update on a menu we will send a payload to the URL you provide us. Please refer to the PosNormalizedMenuUpdateStatus model
The sample payload looks like this.
{
"job_id": "9c09d4a5-cd86-42d3-ad17-4ea3619ce7a6",
"details": "Successful ingestion",
"merchant_statuses": [
{
"merchant_id": "1240234416",
"status": "SUCCESS",
"menu_proof_url": "https://pp.grubhub.com/restaurant/1240234416?proof=true",
"media_validation_error_list": [
{
"media_validation_error_message": "Precondition Failed : invalid url=",
"entity_id": "1835057275",
"external_id": "[item]41fd-8c93-24a87211068e",
"url": "",
"menu_item_name": "(Sand.) Hamburger"
}
]
}
]
}