Benchmark Wine Group API

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].


Base URL

All URLs referenced in the documentation have the following base:

https://api.benchmarkwine.com/v1

Staging Environment

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.

Authentication

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.

Request Header Format

Authorization: Bearer YOUR_API_KEY

GET /products

This 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",
  },
]

Product Attributes

GET /products/:sku

This 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 /orders

This 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
  }
]

Example Response

{
  "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
    }
  ]
}

Possible Errors

{
  "errors": [
    { "sku": "484519", "message": "Insufficient quantity available" },
    { "sku": "440477", "message": "Price has changed since order was placed" }
  ]
}

POST /webhooks

To 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.

Example Request

{
  "url": "https://example.com/bwg_events",
  "events": ["inventory.update"]
}

Example Response

{
  "id": "0190a327-d8e3-7e0b-bf84-1162a31e62fc",
  "url": "https://example.com/bwg_events",
  "events": ["inventory.update"],
  "hmac_secret": "Hgur07imMtczFGnT"
}

Webhook Payload

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.

Retry Schedule

Delivery will be attempted up to 9 times before the notification will be discarded. Retries will follow this schedule after the initial attempt fails:

Webhook Authentication

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 /webhooks

This 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/:id

You 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.