The Benchmark Wine Group API allows you to consume our available products, stay up to date with on going availability changes, and execute orders of items.
If you have any questions you can reach out at [email protected].
All URLs referenced in the documentation have the following base:
https://api.benchmarkwine.com/v1
For testing purposes, we will also provide you with a distinct test API key which can be used at the following base:
https://api.staging.benchmarkwine.com/v1
This is suitable for testing purchases through the POST /orders endpoint. However, due to the relatively low amount of data updates on staging, using it to test webhook integration is not recommended. Webhooks will be called very infrequently compared to production.
All endpoints require authentication to work. We use an API Key based authentication, where you pass your key in a header with each request. All API endpoints will return a 401 status code if the API key is missing or invalid.
Authorization: Bearer YOUR_API_KEY
GET /productsThis endpoint will return the full list of all available products.
[
{
"sku": "440477",
"name": "Giacomo Conterno Barolo Riserva (Speciale) Monfortino 2013 3L",
"quantity": 1,
"vintage": "2013",
"varietal": "Nebbiolo",
"bottle_size": 3000,
"pack_size": 1,
"price": 649200,
"wmj_id": "39287",
"lwin18": null,
},
{
"sku": "440478",
"name": "Giacomo Conterno Barolo Riserva (Speciale) Monfortino 2014 3L",
"quantity": 1,
"vintage": "2014",
"varietal": "Nebbiolo",
"bottle_size": 3000,
"pack_size": 1,
"price": 649200,
"wmj_id": "980732",
"lwin18": null,
},
{
"sku": "484519",
"name": "Domaine des Roches Neuves (T. Germain) Saumur Blanc Clos du Moulin 2017 6 pack",
"quantity": 5,
"vintage": "2017",
"varietal": "Interesting Whites",
"bottle_size": 750,
"pack_size": 6,
"price": 26900,
"wmj_id": "928723",
"lwin18": "837263820170600750",
},
]
sku string - The unique identifier for the productname string - The name of the productquantity integer - The number of units of this product availablevintage string - The vintage of the product. Can be "NV" for non-vintage productsvarietal string - The varietal of the productbottle_size integer - The size of the bottle in milliliterspack_size integer - The number of bottles in a single unit of the productprice integer - The price of the product in cents of USDwmj_id string - The Wine Market Journal ID of the productlwin18 string | null - The Liv-ex Wine Identification Number of the product (if available)GET /products/:skuThis endpoint will return the full details of a single product. It can be useful for fetching information right before an order is placed to confirm that the product is still available in the desired quantity, and that the price has not changed.
GET /products/484519
{
"sku": "484519",
"name": "Domaine des Roches Neuves (T. Germain) Saumur Blanc Clos du Moulin 2017 6 pack",
"quantity": 5,
"vintage": "2017",
"varietal": "Interesting Whites",
"bottle_size": 750,
"pack_size": 6,
"price": 26900,
"wmj_id": "928723",
"lwin18": "837263820170600750",
}
This endpoint will return a 404 status code if the product is not found. This can happen if the product has been removed from our inventory, or if the SKU is incorrect.
POST /ordersThis endpoint is used to place an order for one or more products. The request body should be a JSON array of objects representing the line items of the order. The line items require three fields: sku, quantity, and price. The price field is used to verify that the price of the product has not changed since the time the order was placed. If the price has changed on any line item, the entire order will be rejected. The price should be the same value returned by the /products or /products/:sku endpoints— the price as an integer value of cents of USD.
[
{
"sku": "484519",
"quantity": 2,
"price": 26900
},
{
"sku": "440477",
"quantity": 1,
"price": 649200
}
]
{
"id": 297238,
"order_number": "183726",
"subtotal": 703000,
"tax": 49000,
"total": 967100,
"line_items": [
{
"sku": "484519",
"name": "Domaine des Roches Neuves (T. Germain) Saumur Blanc Clos du Moulin 2017 6 pack",
"vintage": "2017",
"varietal": "Interesting Whites",
"bottle_size": 750,
"pack_size": 6,
"quantity": 2,
"price": 26900,
"total": 53800
},
{
"sku": "440477",
"name": "Giacomo Conterno Barolo Riserva (Speciale) Monfortino 2013 3L",
"vintage": "2013",
"varietal": "Nebbiolo",
"bottle_size": 3000,
"pack_size": 1,
"quantity": 1,
"price": 649200,
"total": 649200
}
]
}
400 - If the request body is not a valid JSON array, or if any of the line items are missing the required fields, then a 400 Bad Request status code will be returned.402 - If your account has a payment limit, and the order would exceed the limit, then a 402 Payment Required status code will be returned.403 - If the quantity requested for any line item is greater than the available quantity, or if the price of any line item is different than the current price of the product, then a 403 Forbidden status code will be returned along with a JSON object describing the error.{
"errors": [
{ "sku": "484519", "message": "Insufficient quantity available" },
{ "sku": "440477", "message": "Price has changed since order was placed" }
]
}
POST /webhooksTo stay in sync with changes to our available products, you can create a webhook subscription using this endpoint. You will register an endpoint that we will send requests to upon changes to our available inventory.
In order to mitigate problems of out of order responses, the update messages will come in the form of the current version of whatever resource has changed. So if an item's quantity has changed from 2 to 1, you will get the full representation of that item at the time the webhook is sent, not at the time the event occurred. This will prevent stale information from being sent out in the event that webhooks get delayed due to a problem on our end. You can be sure that when you receive the webhook, the information contained within is up to date.
{
"url": "https://example.com/bwg_events",
"events": ["inventory.update"]
}
{
"id": "0190a327-d8e3-7e0b-bf84-1162a31e62fc",
"url": "https://example.com/bwg_events",
"events": ["inventory.update"],
"hmac_secret": "Hgur07imMtczFGnT"
}
Our server will hit your endpoint with a POST request that contains event payloads for one or more events. Events may be grouped to prevent sending many requests at once.
{
"events": [
{
"type": "inventory.update",
"payload": {
"sku": "484519",
"name": "Domaine des Roches Neuves (T. Germain) Saumur Blanc Clos du Moulin 2017 6 pack",
"quantity": 4,
"vintage": "2017",
"varietal": "Interesting Whites",
"bottle_size": 750,
"pack_size": 6,
"price": 26900,
"wmj_id": "928723",
"lwin18": "837263820170600750"
}
}
]
}
The payload of an inventory.update event will be the full product details of a new or existing product. When a product is depleted or removed for some reason, it will have a payload with a quantity of 0, and you can remove it from your own list if you would like to.
Your server should respond with a 2xx response in a timely manner. If processing events will take a while, it is best to perform the work asynchronously and respond with a 2xx response as soon as possible. If the request times out, or we do not receive a 2xx response code, the webhook will be retried.
Delivery will be attempted up to 9 times before the notification will be discarded. Retries will follow this schedule after the initial attempt fails:
You may choose to verify that the webhooks are sent from us by checking the X-Webhook-Signature header, which will contain an HMAC-SHA256 signature of the request payload.
Here's an example of how you might do that in Ruby:
require 'openssl'
def verify_signature(payload_body, signature, secret)
computed_signature = OpenSSL::HMAC.hexdigest('SHA256', secret, payload_body)
secure_compare(computed_signature, signature)
end
# A timing-safe compare method to prevent timing attacks
def secure_compare(a, b)
return false if a.empty? || b.empty? || a.bytesize != b.bytesize
result = 0
a.each_byte.zip(b.each_byte) do |x, y|
result |= x ^ y
end
result == 0
end
It is recommended that if a request fails this verification check that you return a 2xx response and not do any additional handling of the event. This is for two reasons: first is that it prevents a malicious 3rd party from being able to guess and check the HMAC secret. Second is that if you need to rotate the HMAC secret, you can easily do so without triggering retries from requests sent with the old secret's signature.
If you need to rotate the HMAC secret for any reason, you can create a new webhook subscription for the same URL, and delete the existing one when you've updated the verification to use the new HMAC secret. During this time you will receive two webhook requests for every one notification, until you delete the old webhook subscription.
GET /webhooksThis will list out all webhook registered under your account. For security, it will not include the HMAC secret in the response.
[
{
"id": "01906ecc-ada3-7151-86f2-a7f458028047",
"url": "https://example.com/bwg_events",
"created_at": "2024-07-01T15:19:00.443Z",
"events": ["inventory.update"]
},
{
"id": "01906ee1-d902-72bb-a3f9-22fc762fdde2",
"url": "https://example.com/bwg_events",
"created_at": "2024-07-01T15:20:43.702Z",
"events": ["inventory.update"]
}
]
DELETE /webhooks/:idYou can use this endpoint to remove an existing webhook. Once a webhook is deleted, no further requests will be made for that subscription, even if there were messages waiting to be retried.
Valid responses will be 204 No Content if the webhook is deleted, or 404 Not Found if the webhook id is not found.