Skip to main content
Version: 3.x

Asynchronous Webhooks

Architecture

Asynchronous webhooks can be used for reacting to the events emitted by Saleor. The worker instance will send the event payload via the chosen protocol. Your App should acknowledge receiving that payload. The result of the App operations is not sent back to the Saleor. If you need to send data back to the user (for example, calculated shipping cost), you should use a synchronous webhook.

Examples of the use cases

  • Sending an order confirmation to the company chat.
  • Synchronizing stocks with an external warehousing system.
  • Updating Elasticsearch or Algolia indexes after changing product descriptions.
  • Triggering frontend SSR for new pages.

Permissions

Managing webhooks is available for users with the MANAGE_APPS permission. The App can also create and modify its own webhooks without the need for additional permissions.

Webhook protocols

Asynchronous webhooks support the following protocols:

  • HTTP(S)
  • Google Cloud Pub/Sub
  • AWS SQS

A protocol is chosen based on the targetUrl scheme.

HTTP(S)

Webhook payloads are sent in POST requests.

All webhooks with targetUrl, where the scheme is HTTP(S) will use this protocol.

If a successful response has not been received in the case of temporary service unavailability, it will retry its request after a delay in time.

While HTTPS webhooks are a familiar concept and may seem easy to implement, ensure your endpoint can handle the same level of concurrency that you expect from the monitored events. Saleor Cloud will do its best to deliver webhooks in real time, which could mean more traffic than your servers can handle. Because of this and the possibility of losing events when the destination server is down, we recommend using a queue like Google Cloud Pub/Sub or AWS SQS (see below).

Headers

During HTTPS requests there are several headers included:

  • Saleor-Event - defines an event which is assigned to the webhook
  • Saleor-Domain - defines a saleor domain
  • Saleor-Signature - defines a signature to indicate that the request is verifiable
warning

In Saleor 4.0 all X-Saleor- headers will be removed and replaced with headers without X- prefix.

Deprecated headers list:

  • X-Saleor-Event -> Saleor-Event
  • X-Saleor-Domain -> Saleor-Domain
  • X-Saleor-Signature -> Saleor-Signature

Payload signature

Saleor calculates a payload signature under Saleor-Signature:

  • if secretKey was set for a webhook - the HMAC SHA-256 header based on it and the payload
  • if secretKey wasn't set for a webhook - the JWS signature using RS256 with payload detached, to verify the signature you can use a public key, which can be fetched from http(s)://<your-backend-domain>/.well-known/jwks.json (available since Saleor 3.5, before 3.5 the payload from webhooks without secretKey set were not signed)
Validating signature

To validate signatures in Saleor Apps, use withWebhookSignatureVerified middleware provided by saleor-app-sdk

import type { Handler } from "retes";
import { Response } from "retes/response";
import { toNextHandler } from "retes/adapter";
import {
withSaleorEventMatch,
withWebhookSignatureVerified,
} from "@saleor/app-sdk/middleware";

const handler: Handler = async (request) => {
// ...

return Response.OK({ success: true });
};

export default toNextHandler([
withSaleorDomainMatch,
withWebhookSignatureVerified(),
handler,
]);

Example taken from saleor-app-template

You can also manually validate the JWS signature in JavaScript with the jose package. Remember that you need to supply it with a raw body string, not a parsed object.

import getRawBody from "raw-body";

const JWKS = jose.createRemoteJWKSet(
new URL("https://master.staging.saleor.cloud" + "/.well-known/jwks.json")
);

// In Next.js you need to disable `bodyParser` in order to get raw body string
// https://github.com/vercel/next.js/discussions/12517
export const config = {
api: {
bodyParser: false,
},
};

export default function(req, res) {
const jws = req.headers["saleor-signature"];
const buffer = getRawBody(req, {
length: req.headers["content-length"],
limit: "1mb",
});
const [header, _, signature] = jws.split(".");
try {
await jose.flattenedVerify({
protected: header,
payload: buffer.toString("utf-8"),
signature
}, JWKS);
} catch (e) {
// return error
}

// handle your request
}

Google Cloud Pub/Sub

All webhooks with the gcpubsub scheme will be treated as the Google Cloud Pub/Sub webhooks. The structure of the targetUrl scheme should be as below:

gcpubsub://cloud.google.com/projects/<yourproject>/topics/<yourtopic>

To use the Google Cloud Pub/Sub properly, you need to set the GOOGLE_APPLICATION_CREDENTIALS environment variable.

AWS SQS

All webhooks with the awssqs scheme will be treated as AWS SQS webhooks.

The structure of targetUrl should be as below:

awssqs://<access_key_id>:<secret_access_key>@sqs.<region>.amazonaws.com/<account_id>/<queue_name>
note

FIFO queues (ending in .fifo) must be configured to have ContentBasedDeduplication enabled. MessageGroupID will be set to current Site's domain name.

Available webhook events

Webhooks can be used to receive data from Saleor when particular events happen. The available events are:

NameDescription
ANY_EVENTSReceive webhooks when any of the available events is triggered (wildcard event).
ADDRESS_CREATEDA new address is created.
ADDRESS_UPDATEDAn address is updated.
ADDRESS_DELETEDAn address is deleted.
APP_INSTALLEDA new App installed.
APP_UPDATEDAn App is updated.
APP_DELETEDAn App is deleted.
APP_STATUS_CHANGEDAn App status is changed.
ATTRIBUTE_CREATEDA new attribute created.
ATTRIBUTE_UPDATEDAn attribute updated.
ATTRIBUTE_DELETEDAn attribute deleted.
CATEGORY_CREATEDA new category created.
CATEGORY_UPDATEDA category is updated.
CATEGORY_DELETEDA category is deleted.
CHANNEL_CREATEDA new channel is created.
CHANNEL_UPDATEDA channel is updated.
CHANNEL_DELETEDA channel is deleted.
CHANNEL_STATUS_CHANGEDA channel status is changed.
GIFT_CARD_CREATEDA new gift card is created.
GIFT_CARD_UPDATEDA gift card is updated.
GIFT_CARD_DELETEDA gift card is deleted.
GIFT_CARD_STATUS_CHANGEDA gift card status is changed.
CHECKOUT_CREATEDA new checkout is created.
CHECKOUT_UPDATEDA checkout is updated. Also triggers for all updates related to a checkout.
CUSTOMER_CREATEDA new customer account is created.
CUSTOMER_UPDATEDA customer account is updated.
CUSTOMER_DELETEDA customer account is deleted.
FULFILLMENT_CREATEDA new fulfillment is created.
INVOICE_DELETEDAn invoice is deleted.
INVOICE_REQUESTEDAn invoice for an order is requested. At this stage number, created and external_url fields are nulls.
INVOICE_SENTAn invoice is sent.
MENU_CREATEDA new menu is created.
MENU_UPDATEDA menu is updated.
MENU_DELETEDA menu is deleted.
MENU_ITEM_CREATEDA new menu item is created.
MENU_ITEM_UPDATEDA menu item is updated
MENU_ITEM_DELETEDA menu item is deleted.
ORDER_CANCELLEDAn order is cancelled.
ORDER_CONFIRMEDAn order is confirmed (status change unconfirmed -> unfulfilled) by staff user using OrderConfirm mutation. Also triggers when the user finishes checkout and the shop setting automatically_confirm_all_new_orders is enabled.
ORDER_CREATEDA new order is placed.
ORDER_FULFILLEDAn order is fulfilled.
ORDER_FULLY_PAIDPayment is made and an order is fully paid.
ORDER_UPDATEDAn order is updated; triggered for all changes related to an order; covers all other order webhooks, except for ORDER_CREATED.
SALE_CREATEDA new sale is created.
SALE_UPDATEDA sale is updated.
SALE_DELETEDA sale is deleted.
SALE_TOGGLEA sale is starting or ending.
PAGE_CREATEDA new page is created.
PAGE_DELETEDA page is deleted.
PAGE_UPDATEDA page is updated.
PAGE_TYPE_CREATEDA new page type is created.
PAGE_TYPE_UPDATEDA page type is updated.
PAGE_TYPE_DELETEDA page type is deleted.
COLLECTION_CREATEDA new collection is created.
COLLECTION_DELETEDA collection is deleted.
COLLECTION_UPDATEDA collection is updated.
PRODUCT_CREATEDA new product is created.
PRODUCT_DELETEDA product is deleted.
PRODUCT_UPDATEDA product is updated.
PRODUCT_VARIANT_CREATEDA new product variant is created.
PRODUCT_VARIANT_DELETEDA product variant is deleted.
PRODUCT_VARIANT_UPDATEDA product variant is updated.
NOTIFYTrigger notification to user.
TRANSACTION_ACTION_REQUESTAn action request for a transaction. Called when the state of the transaction should be changed. See sample payload.
TRANSLATION_CREATEDA new translation is created.
TRANSLATION_UPDATEDA translation is updated.
PRODUCT_VARIANT_OUT_OF_STOCKA product variant is out of stock.
PRODUCT_VARIANT_BACK_IN_STOCKA product variant is back in stock.
SHIPPING_PRICE_CREATEDA new shipping price created.
SHIPPING_PRICE_UPDATEDA shipping price is updated.
SHIPPING_PRICE_DELETEDA shipping price is deleted.
SHIPPING_ZONE_CREATEDA new shipping zone created.
SHIPPING_ZONE_UPDATEDA shipping zone is updated.
SHIPPING_ZONE_DELETEDA shipping zone is deleted.
STAFF_CREATEDA new staff user is created.
STAFF_UPDATEDA staff user is updated.
STAFF_DELETEDA staff user is deleted.
VOUCHER_CREATEDA new voucher is created.
VOUCHER_UPDATEDA voucher is updated.
VOUCHER_DELETEDA voucher is deleted.
WAREHOUSE_CRATEDA new warehouse is created.
WAREHOUSE_UPDATEDA warehouse is updated.
WAREHOUSE_DELETEDA warehouse is deleted.

Managing app webhooks

After installation, the App can create a webhook subscription. To manage its own webhooks, no additional permissions are needed. If requests contain the app token in the Authentication header, the app argument will be automatically populated with the corresponding App.

Subscribing to a webhook

To subscribe to a webhook, we need to first create an App with proper permissions. Let's assume that we want to extend the order processing app. The App should receive notifications whenever new orders are created in Saleor. To do so, we'll create a new webhook using the webhookCreate mutation. The mutation takes the following input:

  • name: the name of the webhook.
  • targetUrl: the URL of a service that will receive webhooks requests.
  • events: a list of the events to subscribe to.
  • app: the ID of the App to which the webhook belongs. Can be ommited, if the request has the Authentication header with the App access token.
  • isActive: whether to activate the webhook.
  • secretKey argument is optional, but recommended for validating incoming payloads.
mutation {
webhookCreate(
input: {
name: "New orders notification"
targetUrl: "https://order-processing-service.example.com"
events: [ORDER_CREATED]
app: "QXBwOjk="
isActive: true
secretKey: "secret-key"
}
) {
webhook {
id
}
webhookErrors {
field
code
}
}
}

If there are no errors in the response, the webhook is successfully created. From now on, whenever a new order is placed, the payload with the order data will be sent to your targetUrl.

Updating a webhook

To update a webhook (e.g. to deactivate it or change the permissions), use the webhookUpdate mutation. The mutation takes similar input fields as the webhookCreate mutation. The example below shows how to deactive a webhook:

mutation {
webhookUpdate(id: "V2ViaG9vazox", input: { isActive: false }) {
webhook {
isActive
}
webhookErrors {
field
code
}
}
}

Removing a webhook

To fully remove a webhook, use the webhookDelete mutation:

mutation {
webhookDelete(id: "V2ViaG9vazox") {
webhookErrors {
field
code
}
}
}