Skip to content

Overview

Webhooks are a way to notify your applications, or webhook-enabled portals - like Discord - of events in your store.

You may configure webhooks to be triggered on specific events, such as when a product is created or updated, or when an order is completed (full list of events).

Webhooks are defined in the dashboard and take the following values:

  • Event - Event to trigger this webhook on
  • Method - HTTP method to use when sending the webhook (POST, GET, PUT, DELETE, PATCH)
  • URL - URL to send the webhook to
  • Payload Template - JSON body template for the payload to send. You may use placeholders to include dynamic data in the payload. Only supported for POST, PUT and PATCH methods.
  • Headers - Optional additional headers, in JSON form

Discord Example

You may easily set up Discord notifications using webhooks. This example covers creating a webhook notification for completed orders. However, other events can be used in a similar manner, simply with different body placeholders.

Create the webhook in Discord (Server Settings > Integrations), copy the webhook URL and create a new webhook in the admin dashboard with the following values:

  • Event: order.completed
  • Method: POST
  • URL: Your Discord webhook URL
  • Payload Template:
json
{
  "content": "New order completed by %%USER_NAME%% (%%USER_EMAIL%%) for %%TOTAL%%! Items: %%ITEMS%%"
}
  • Headers (leave empty)

Events

The following events can be used as webhook triggers. Each has a set of predefined placeholders you can use in the payload template.

product.created - Triggered when a product is created in the store

product.created Available Placeholders
  • %%PRODUCT_ID%% - ID of product object

  • %%PRODUCT_NAME%% - Product name

  • %%PRODUCT_VENDOR%% - Product vendor's name

  • %%PRODUCT_URL%% - Fully qualified URL to the product (leads to 404 on deleted event, kept for consistency)

  • %%PRODUCT_COVER_IMAGE_URL%% - Cover image URL

  • %%PRODUCT_PRICE%% - Price in the default store currency (or first if default currency is not found)

  • %%DATE_TIME%% - Date of creation

  • %%TIMESTAMP%% - Date of creation (seconds-based timestamp)

product.deleted - Triggered when a product is deleted from the store:

product.deleted Available Placeholders
  • %%PRODUCT_ID%% - ID of product object

  • %%PRODUCT_NAME%% - Product name

  • %%PRODUCT_VENDOR%% - Product vendor's name

  • %%PRODUCT_URL%% - Fully qualified URL to the product (leads to 404 on deleted event, kept for consistency)

  • %%PRODUCT_COVER_IMAGE_URL%% - Cover image URL

  • %%PRODUCT_PRICE%% - Price in the default store currency (or first if default currency is not found)

  • %%DATE_TIME%% - Date of creation

  • %%TIMESTAMP%% - Date of creation (seconds-based timestamp)

product.discounted - Triggered when a product had the discount field updated and is more than it was before (following that, inherently, this means this event also triggers for discount increases)

product.discounted Available Placeholders
  • %%PRODUCT_ID%% - ID of product object

  • %%PRODUCT_NAME%% - Product name

  • %%PRODUCT_VENDOR%% - Product vendor's name

  • %%PRODUCT_URL%% - Fully qualified URL to the product

  • %%PRODUCT_COVER_IMAGE_URL%% - Cover image URL

  • %%PRODUCT_PRICE%% - New, discounted price in the default store currency (or first if default currency is not found)

  • %%PRODUCT_OLD_PRICE%% - Old price in the default store currency (or first if default currency is not found)

  • %%PRODUCT_DISCOUNT%% - Percentage price discount

  • %%DATE_TIME%% - Date of creation

  • %%TIMESTAMP%% - Date of creation (seconds-based timestamp)

product.updated - Triggered when a product is updated in the store (a new version is released, except initial release of 1.0.0)

product.updated Available Placeholders
  • %%VERSION_ID%% - ID of version object

  • %%VERSION_NUMBER%% - Version number, e.g. v5.2.1

  • %%VERSION_CHANGELOG%% - Full changelog Markdown string

  • %%VERSION_CHANGELOG_HTML%% - Full changelog HTML string

  • %%VERSION_CHANGELOG_EXCERPT%% - Changelog Markdown string trimmed to 100 letters (preserving words)

  • %%PRODUCT_ID%% - ID of product object

  • %%PRODUCT_NAME%% - Product name

  • %%PRODUCT_VENDOR%% - Product vendor's name

  • %%PRODUCT_URL%% - Fully qualified URL to the product

  • %%PRODUCT_COVER_IMAGE_URL%% - Cover image URL

  • %%PRODUCT_PRICE%% - Price in the default store currency (or first if default currency is not found)

  • %%DATE_TIME%% - Date of update

  • %%TIMESTAMP%% - Date of creation (seconds-based timestamp)

order.completed - Triggered when an order is completed (paid)

order.completed Available Placeholders
  • %%ORDER_ID%% - ID of order object

  • %%TOTAL%% - Total price paid formatted to default currency

  • %%USER_ID%% - ID of user who placed the order

  • %%USER_NAME%% - Name of user who placed the order

  • %%USER_EMAIL%% - Email of user who placed the order

  • %%ITEMS%% - List of items in the order, formatted as Quantityx Product Name, separated by commas

  • %%DATE_TIME%% - Date of payment

  • %%TIMESTAMP%% - Date of payment (seconds-based timestamp)

review.created - Triggered when a review is created (available since v1.9.0)

review.created Available Placeholders
  • %%REVIEW_ID%% - ID of review object

  • %%USER_ID%%` - ID of user who placed the review

  • %%USER_NAME%% - Name of user who placed the review

  • %%USER_EMAIL%% - Email of user who placed the review

  • %%PRODUCT_ID%% - ID of product

  • %%PRODUCT_NAME%% - Name of product

  • %%PRODUCT_URL%% - Fully qualified URL to the product

  • %%RATING%% - Rating of the review

  • %%CONTENT%% - Content of the review

  • %%DATE_TIME%% - Date of payment

  • %%TIMESTAMP%% - Date of payment (seconds-based timestamp)

Security

In order to use webhooks as an "only source of truth" you need to trust the data the webhook is sending (for example, if your app relies on "order completed" webhooks to give out licenses), you need to validate the webhook requests coming to your application.

This verification is crucial to block attempts at webhook forgery, where a malicious actor creates a valid-looking body, sends it to your webhook endpoint and gets a free license.

Do I need verification?

Technically, no. Verifying these headers is only essential for webhooks that trigger sensitive actions in your application, such as license generation or user creation. If your webhook is only used for logging or notifications, you may skip this step - the webhooks will work regardless - however, you're at risk of receiving forged data in your notifications.

Doing so is quite straightforward, but the implementation varies on the programming language and framework you are using. The general idea boils down to:

  • Each webhook has a "Signing Secret" assigned to it. You can find it in the webhooks tab in the admin dashboard.
  • A X-Webhook-Signature header is sent with each webhook request. It contains a HMAC SHA256 hash of the request body JSON, signed with the signing secret.
  • To validate the authority of the request, you need to compute your own HMAC SHA256 hash of the request body on your side, using the same signing secret.
  • You may then compare your own hash to the value sent in the X-Webhook-Signature header. If they match, the request is valid.

Example of validation in JavaScript:

js
import crypto from "crypto"; // or const crypto = require("crypto"); for CommonJS

// Your webhook signing secret from the admin dashboard
const SIGNING_SECRET = "e68007b0fb5..."; 

/// ...

// In your request handler:
app.post("/webhook", (req, res) => {
  // The signature sent in the request headers
  const signature = req.headers["x-webhook-signature"];
  // The raw request body as a string or Buffer
  const body = req.rawBody; 
  const hash = crypto.createHmac("sha256", SIGNING_SECRET).update(body).digest("hex");
  if (hash !== signature) {
    // Invalid request, request was possibly forged
    res.status(401).send("Invalid signature");
    return;
  }
  // Valid request, proceed with processing
  /// ...
  // Respond with 200 code
  res.status(200).send("OK");
});

Caveats:

The signature is signed with the raw request body, with unescaped slashes, unescaped unicode characters and not pretty-printed in any way. Make sure you're using the same format when computing the hash on your side.