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:
-
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.
-
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
-
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. -
The webhook handler must respond to this GET request within 3 seconds with a JSON body containing the
response_token
field. The value ofresponse_token
should be the HMAC SHA256 hash of thecrc_token
, base64-encoded. Thecrc_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)) + `"}`)) }
-
The CRC check is performed every hour.
-
If the webhook endpoint fails the CRC for 6 consecutive hours, the webhook will transition from the
ok
state to thefailed
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 Type | Payload |
---|---|
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; } |