Verifying Incoming Webhook Requests

To prevent malicious actors from impersonating ShopSurvey and sending unauthenticated requests to your Webhook URL, we sign each Webhook request we send using HMAC cryptographic verification.

Verifying Webhook Request Headers Using HMAC Cryptographic Verification

To verify the authenticity of Webhook Requests, we suggest verifying the incoming X-SHOPSURVEY-WEBHOOK-HMAC-TOKEN on each incoming request. To do this, follow these steps:

  1. On each incoming request, retrieve the values of the following headers:

    1. X-SHOPSURVEY-WEBHOOK-TOPIC

    2. X-SHOPSURVEY-WEBHOOK-SENT-AT

    3. X-SHOPSURVEY-WEBHOOK-REQUEST-ID

    4. X-SHOPSURVEY-WEBHOOK-ATTEMPT

    5. X-SHOPSURVEY-WEBHOOK-MESSAGE-ID

    6. X-SHOPSURVEY-WEBHOOK-ID

    7. X-SHOPSURVEY-WEBHOOK-HMAC-ALGORITHM

    8. X-SHOPSURVEY-WEBHOOK-HMAC

  2. Load these headers into a dictionary except for X-SHOPSURVEY-WEBHOOK-HMAC

  3. Sort the dictionary by key into ascending alphabetical order and transform it into a JSON string representing the HMAC payload

  4. Use this generated payload, your unique Webhook HMAC Secret (found under Webhook settings), and the provided X-SHOPSURVEY-WEBHOOK-HMAC-ALGORITHM (SHA256) to calculate the HMAC hexdigest

  5. Compare the calculated HMAC with the value in X-SHOPSURVEY-WEBHOOK-HMAC, if these values are the same, you can process the webhook request.

Here's an example of performing this logic in Ruby on Rails:

class MyWebhookController < ApplicationController

  def process_webhook

    # slice out the relevant headers into their own hash.
    # notice we DO NOT include the hmac itself here.
    hmac_params = request.headers.slice(
      'X-SHOPSURVEY-WEBHOOK-TOPIC',
      'X-SHOPSURVEY-WEBHOOK-SENT-AT',
      'X-SHOPSURVEY-WEBHOOK-REQUEST-ID',
      'X-SHOPSURVEY-WEBHOOK-ATTEMPT',
      'X-SHOPSURVEY-WEBHOOK-MESSAGE-ID',
      'X-SHOPSURVEY-WEBHOOK-ID',
      'X-SHOPSURVEY-WEBHOOK-HMAC-ALGORITHM'
    )

    # get the hmac and algo from the request headers
    hmac = request.headers['X-SHOPSURVEY-WEBHOOK-HMAC']
    hmac_algo = request.headers['X-SHOPSURVEY-WEBHOOK-HMAC-ALGORITHM']

    # get the webhook hmac secret from our account and save it into
    # an environment variable
    secret = ENV.fetch('SHOPSURVEY_WEBHOOK_HMAC_SECRET', nil)
    raise StandardError, "No Webhook HMAC Secret configured in ENV" if hmac_signing_secret.nil?

    # calculate the HMAC from the params
    payload = hmac_params.sort.to_h.to_json
    calculated_hmac = OpenSSL::HMAC.hexdigest(hmac_algo, secret, payload)

    unless ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac)
      raise StandardError, "HMAC Verification Failed"
    end

    ## the request is valid. process the webhook payload now...
    ## ideally, enqueue it and process it in a separate worker.
    ## you need to return a response in under 5 seconds.
    ## for example...
    my_webhook_data = {
      topic: request.headers['X-SHOPSURVEY-WEBHOOK-TOPIC'],
      payload: JSON.parse(request.body.read)
    }
    MyWebhookProcessingJob.perform_later(my_webhook_data.to_json)

    render json: {}, status: :ok
  end
end

Webhook Requirements

Your webhook endpoint must return a 200 OK Response within a 5-second timeout, or we will cancel the request and retry up to 2 more times (3 attempts total) to send the same Webhook message.

We highly recommend your endpoint verify the authenticity of the request and then internally enqueue the webhook message to be processed in a background worker (something like Sidekiq or Celery). This ensures that you aren't exceeding the timeout limit or missing webhook events. It also allows you to replay them internally if your processing logic contains a bug or raises an exception while running.

Last updated