Concepts > Webhooks
OverviewConceptsWebhooks
Webhooks

General

Webhooks serve as a way for your app to receive notifications from Lana when specific events occur. They eliminate the need for your app to continuously poll the API by providing notifications about changes. When set up, webhooks send either all or selected events to an HTTP endpoint in the form of a JSON payload. The general format of a message is as follows:

interface WebhookData {
  event_id: string;
  event_type: string;
  created_at: string;
  payload: Payload;
}

It's important to remember that you should not blindly trust the origin of a webhook to be Lana. Even though it's unlikely, forgeries can happen. Therefore, it's prudent to perform an authenticated API request using the IDs in the webhook to verify the event's authenticity. By design, Lana does not include any personally identifiable information in webhooks.

When Lana triggers a webhook event, it carries out a POST request to a URL specified in the webhook. This request is attempted thrice in a row with a timeout of 30 seconds and pauses of 1 second and 5 seconds between attempts. This sequence is regarded as a single attempt.

The webhook is considered successful only if the response code falls within the 2xx range. If all three requests fail, Lana will attempt to deliver the webhook at a later time. The following is a rough schedule for further webhook retry attempts (±5 minutes):

  • 30 minutes
  • 60 minutes
  • 90 minutes
  • 2 hours
  • 3 hours
  • 4 hours
  • 8 hours
  • 16 hours
  • 24 hours
  • 36 hours

Challenge-Response Check (CRC) for Webhooks

Overview

Challenge-Response Check (CRC) is a security feature implemented for webhooks to ensure the authenticity and integrity of the communication between the webhook sender and the webhook handler. This documentation provides an overview of CRC and the necessary steps for webhook handlers to implement it.

Webhook Fields

Each webhook API object contains two fields:

  1. crc_status: Represents the status of the webhook's CRC. It can have the following values:

    • pending: The initial state of all newly created webhooks. Webhooks in this state can still receive events.
    • failed: Indicates that the webhook has failed the CRC. Webhooks in this state will not receive new events but may still receive retries for old events.
    • ok: Indicates that the webhook has successfully passed the CRC and can receive events normally.
  2. secret: A string that serves as the "secret" portion of the HMAC (Hash-based Message Authentication Code) used for CRC and webhook content validation.

CRC Check Process

  1. The CRC check is performed by sending a GET request to the webhook endpoint with the query parameter ?crc_token=<random>, where <random> is a randomly generated token.

  2. The webhook handler must respond to this GET request within 3 seconds with a JSON body containing the response_token field. The value of response_token should be the HMAC SHA256 hash of the crc_token, base64-encoded. The crc_token should be used as the "message" portion of the HMAC, without any encoding.

    Example response:

    {
      "response_token": "sha256=<hmac_sha256_hash_base64_encoded>"
    }
    

    In Go the correct GET request handler looks like this:

    const webhookSecret = `74e3f83b8b73f456ac0d731aecf73e8b39a6aceecd67c544eae124cd08cd26ad`;
    func handleCRC(w http.ResponseWriter, r *http.Request) {
      token := r.URL.Query().Get("crc_token")
      hmacHash := hmac.New(sha256.New, []byte(webhookSecret))
      hmacHash.Write([]byte(token))
      w.Header().Set("Content-Type", "application/json")
      w.WriteHeader(200)
      w.Write([]byte(`{"response_token":"sha256=` + base64.StdEncoding.EncodeToString(hmacHash.Sum(nil)) + `"}`))
    }
    
  3. The CRC check is performed every hour.

  4. If the webhook endpoint fails the CRC for 6 consecutive hours, the webhook will transition from the ok state to the failed state. All other state transitions are immediate.

Webhook Content Validation

In addition to the CRC, the secret can be used to validate the webhook POST body and the GET request sent during the CRC. The X-Webhook-Signature header is set on webhook requests, containing the HMAC SHA256 hash (base64-encoded) of the request body or the crc_token query parameter.

  • For POST requests, the "message" of the HMAC is the body of the request.
  • For GET requests (CRC), the "message" of the HMAC is the string crc_token=<crc_token_query_parameter>.

Use the following Go functions as examples:

const webhookSecret = `74e3f83b8b73f456ac0d731aecf73e8b39a6aceecd67c544eae124cd08cd26ad`;

func validateCRCRequest(r *http.Request) {
  token := r.URL.Query().Get("crc_token")
  hmacHash := hmac.New(sha256.New, []byte(webhookSecret))
  hmacHash.Write([]byte("crc_token=" + token))
  expected := "sha256=" + base64.StdEncoding.EncodeToString(hmacHash.Sum(nil))
  if s := r.Header.Get("X-Webhook-Signature"); s != expected {
    panic("invalid webhook signature")
  }
}

func validateWebhookPayload(r *http.Request) {
  body, _ := io.ReadAll(r.Body)
  r.Body.Close()
  hmacHash := hmac.New(sha256.New, []byte(webhookSecret))
  hmacHash.Write(body)
  expected := "sha256=" + base64.StdEncoding.EncodeToString(hmacHash.Sum(nil))
  if s := r.Header.Get("X-Webhook-Signature"); s != expected {
    panic("invalid webhook signature")
  }
}

Manual CRC Invocation

The webhooksCrc endpoint allows manual invocation of the CRC on any webhook. This can be useful to instantly test CRC handler without having to wait 1 hour for automatic check.

Event Types and Payloads

Event TypePayload
brand.created
interface Payload {
  shop_id string;
  payload string;
}
brand.deleted
interface Payload {
  shop_id string;
  payload string;
}
brand.modified
interface Payload {
  shop_id string;
  payload string;
}
buy_link.created
interface Payload {
  shop_id string;
  payload string;
}
buy_link.deleted
interface Payload {
  shop_id string;
  payload string;
}
buy_link.modified
interface Payload {
  shop_id string;
  payload string;
}
category.created
interface Payload {
  shop_id string;
  payload string;
}
category.deleted
interface Payload {
  shop_id string;
  payload string;
}
category.modified
interface Payload {
  shop_id string;
  payload string;
}
content_block.created
interface Payload {
  shop_id string;
  payload string;
}
content_block.deleted
interface Payload {
  shop_id string;
  payload string;
}
content_block.modified
interface Payload {
  shop_id string;
  payload string;
}
customer.created
interface Payload {
  shop_id string;
  payload string;
}
customer.deleted
interface Payload {
  shop_id string;
  payload string;
}
customer.modified
interface Payload {
  shop_id string;
  payload string;
}
customer_balance.transaction
interface Payload {
  shop_id string;
  payload string;
}
customer_group.created
interface Payload {
  shop_id string;
  payload string;
}
customer_group.deleted
interface Payload {
  shop_id string;
  payload string;
}
customer_group.modified
interface Payload {
  shop_id string;
  payload string;
}
customer_inventory_note.created
interface Payload {
  shop_id string;
  payload string;
}
customer_inventory_note.deleted
interface Payload {
  shop_id string;
  payload string;
}
customer_inventory_note.modified
interface Payload {
  shop_id string;
  payload string;
}
customer_note.created
interface Payload {
  shop_id string;
  payload string;
}
customer_note.deleted
interface Payload {
  shop_id string;
  payload string;
}
customer_note.modified
interface Payload {
  shop_id string;
  payload string;
}
data_feed.created
interface Payload {
  shop_id string;
  payload string;
}
data_feed.deleted
interface Payload {
  shop_id string;
  payload string;
}
data_feed.modified
interface Payload {
  shop_id string;
  payload string;
}
domain.created
interface Payload {
  shop_id string;
  payload string;
}
domain.deleted
interface Payload {
  shop_id string;
  payload string;
}
file.created
interface Payload {
  shop_id string;
  payload string;
}
file.deleted
interface Payload {
  shop_id string;
  payload string;
}
fulfillment.created
interface Payload {
  shop_id string;
  payload string;
}
fulfillment.modified
interface Payload {
  shop_id string;
  payload string;
}
gateway.created
interface Payload {
  shop_id string;
  payload string;
}
gateway.deleted
interface Payload {
  shop_id string;
  payload string;
}
gateway.modified
interface Payload {
  shop_id string;
  payload string;
}
inventory_location.created
interface Payload {
  shop_id string;
  payload string;
}
inventory_location.modified
interface Payload {
  shop_id string;
  payload string;
}
inventory_location.relocate
interface Payload {
  shop_id string;
  payload string;
}
inventory_rule.created
interface Payload {
  shop_id string;
  payload string;
}
inventory_rule.deleted
interface Payload {
  shop_id string;
  payload string;
}
inventory_rule.modified
interface Payload {
  shop_id string;
  payload string;
}
key.added
interface Payload {
  shop_id string;
  payload string;
}
key.deleted
interface Payload {
  shop_id string;
  payload string;
}
menu.created
interface Payload {
  shop_id string;
  payload string;
}
menu.deleted
interface Payload {
  shop_id string;
  payload string;
}
menu.modified
interface Payload {
  shop_id string;
  payload string;
}
option.created
interface Payload {
  shop_id string;
  payload string;
}
option.deleted
interface Payload {
  shop_id string;
  payload string;
}
option.modified
interface Payload {
  shop_id string;
  payload string;
}
option_set.created
interface Payload {
  shop_id string;
  payload string;
}
option_set.deleted
interface Payload {
  shop_id string;
  payload string;
}
option_set.modified
interface Payload {
  shop_id string;
  payload string;
}
order.abandoned
interface Payload {
  shop_id string;
  payload string;
}
order.canceled
interface Payload {
  shop_id string;
  payload string;
}
order.captured
interface Payload {
  shop_id string;
  payload string;
}
order.deleted
interface Payload {
  shop_id string;
  payload string;
}
order.imported
interface Payload {
  shop_id string;
  payload string;
}
order.modified
interface Payload {
  shop_id string;
  payload string;
}
order.paid
interface Payload {
  shop_id string;
  payload string;
}
order.refunded
interface Payload {
  shop_id string;
  payload string;
}
order.reserved
interface Payload {
  shop_id string;
  payload string;
}
order.restocked
interface Payload {
  shop_id string;
  payload string;
}
order.unabandoned
interface Payload {
  shop_id string;
  payload string;
}
order.unreserved
interface Payload {
  shop_id string;
  payload string;
}
order_note.created
interface Payload {
  shop_id string;
  payload string;
}
order_note.deleted
interface Payload {
  shop_id string;
  payload string;
}
order_note.modified
interface Payload {
  shop_id string;
  payload string;
}
order_risk.created
interface Payload {
  shop_id string;
  payload string;
}
order_risk.deleted
interface Payload {
  shop_id string;
  payload string;
}
packaging.created
interface Payload {
  shop_id string;
  payload string;
}
packaging.deleted
interface Payload {
  shop_id string;
  payload string;
}
packaging.modified
interface Payload {
  shop_id string;
  payload string;
}
payment_method.created
interface Payload {
  shop_id string;
  payload string;
}
payment_method.deleted
interface Payload {
  shop_id string;
  payload string;
}
payment_method.modified
interface Payload {
  shop_id string;
  payload string;
}
product.created
interface Payload {
  shop_id string;
  payload string;
}
product.deleted
interface Payload {
  shop_id string;
  payload string;
}
product.modified
interface Payload {
  shop_id string;
  payload string;
}
product_field_set.created
interface Payload {
  shop_id string;
  payload string;
}
product_field_set.deleted
interface Payload {
  shop_id string;
  payload string;
}
product_field_set.modified
interface Payload {
  shop_id string;
  payload string;
}
promotion.created
interface Payload {
  shop_id string;
  payload string;
}
promotion.deleted
interface Payload {
  shop_id string;
  payload string;
}
promotion.modified
interface Payload {
  shop_id string;
  payload string;
}
purchase_order.created
interface Payload {
  shop_id string;
  payload string;
}
purchase_order.deleted
interface Payload {
  shop_id string;
  payload string;
}
purchase_order.modified
interface Payload {
  shop_id string;
  payload string;
}
purchase_order.received
interface Payload {
  shop_id string;
  payload string;
}
purchase_order.rejected
interface Payload {
  shop_id string;
  payload string;
}
purchase_order_note.created
interface Payload {
  shop_id string;
  payload string;
}
purchase_order_note.deleted
interface Payload {
  shop_id string;
  payload string;
}
purchase_order_note.modified
interface Payload {
  shop_id string;
  payload string;
}
return.approved
interface Payload {
  shop_id string;
  payload string;
}
return.created
interface Payload {
  shop_id string;
  payload string;
}
return.deleted
interface Payload {
  shop_id string;
  payload string;
}
return.modified
interface Payload {
  shop_id string;
  payload string;
}
return.rejected
interface Payload {
  shop_id string;
  payload string;
}
return_note.created
interface Payload {
  shop_id string;
  payload string;
}
return_note.deleted
interface Payload {
  shop_id string;
  payload string;
}
return_note.modified
interface Payload {
  shop_id string;
  payload string;
}
return_policy.created
interface Payload {
  shop_id string;
  payload string;
}
return_policy.deleted
interface Payload {
  shop_id string;
  payload string;
}
return_policy.modified
interface Payload {
  shop_id string;
  payload string;
}
review_dimension.created
interface Payload {
  shop_id string;
  payload string;
}
review_dimension.deleted
interface Payload {
  shop_id string;
  payload string;
}
review_dimension.modified
interface Payload {
  shop_id string;
  payload string;
}
review_dimension_set.created
interface Payload {
  shop_id string;
  payload string;
}
review_dimension_set.deleted
interface Payload {
  shop_id string;
  payload string;
}
review_dimension_set.modified
interface Payload {
  shop_id string;
  payload string;
}
role.created
interface Payload {
  shop_id string;
  payload string;
}
role.deleted
interface Payload {
  shop_id string;
  payload string;
}
role.modified
interface Payload {
  shop_id string;
  payload string;
}
sales_channel.created
interface Payload {
  shop_id string;
  payload string;
}
sales_channel.deleted
interface Payload {
  shop_id string;
  payload string;
}
sales_channel.modified
interface Payload {
  shop_id string;
  payload string;
}
scheduled_fulfillment.created
interface Payload {
  shop_id string;
  payload string;
}
scheduled_fulfillment.deleted
interface Payload {
  shop_id string;
  payload string;
}
scheduled_fulfillment.modified
interface Payload {
  shop_id string;
  payload string;
}
shipping_provider.created
interface Payload {
  shop_id string;
  payload string;
}
shipping_provider.deleted
interface Payload {
  shop_id string;
  payload string;
}
shipping_provider.modified
interface Payload {
  shop_id string;
  payload string;
}
shipping_rule.created
interface Payload {
  shop_id string;
  payload string;
}
shipping_rule.deleted
interface Payload {
  shop_id string;
  payload string;
}
shipping_rule.modified
interface Payload {
  shop_id string;
  payload string;
}
shop.deleted
interface Payload {
  shop_id string;
  payload string;
}
shop.modified
interface Payload {
  shop_id string;
  payload string;
}
shop.plan_updated
interface Payload {
  shop_id string;
  payload string;
}
shop_settings.modified
interface Payload {
  shop_id string;
  payload string;
}
subscription.canceled
interface Payload {
  shop_id string;
  payload string;
}
subscription.created
interface Payload {
  shop_id string;
  payload string;
}
subscription.failed
interface Payload {
  shop_id string;
  payload string;
}
subscription.pending_payment
interface Payload {
  shop_id string;
  payload string;
}
subscription.upcoming_payment
interface Payload {
  shop_id string;
  payload string;
}
supplier.created
interface Payload {
  shop_id string;
  payload string;
}
supplier.deleted
interface Payload {
  shop_id string;
  payload string;
}
supplier.modified
interface Payload {
  shop_id string;
  payload string;
}
survey_question.created
interface Payload {
  shop_id string;
  payload string;
}
survey_question.deleted
interface Payload {
  shop_id string;
  payload string;
}
survey_question.modified
interface Payload {
  shop_id string;
  payload string;
}
tax_rule.created
interface Payload {
  shop_id string;
  payload string;
}
tax_rule.deleted
interface Payload {
  shop_id string;
  payload string;
}
tax_rule.modified
interface Payload {
  shop_id string;
  payload string;
}
transfer.created
interface Payload {
  shop_id string;
  payload string;
}
transfer.deleted
interface Payload {
  shop_id string;
  payload string;
}
transfer.modified
interface Payload {
  shop_id string;
  payload string;
}
transfer.received
interface Payload {
  shop_id string;
  payload string;
}
transfer.rejected
interface Payload {
  shop_id string;
  payload string;
}
transfer_note.created
interface Payload {
  shop_id string;
  payload string;
}
transfer_note.deleted
interface Payload {
  shop_id string;
  payload string;
}
transfer_note.modified
interface Payload {
  shop_id string;
  payload string;
}
variant.created
interface Payload {
  shop_id string;
  payload string;
}
variant.deleted
interface Payload {
  shop_id string;
  payload string;
}
variant.modified
interface Payload {
  shop_id string;
  payload string;
}
PREVIOUS
Typical Bulk Get Endpoint
NEXT
Markdown Test