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 webhookSaleor-Domain
- defines a saleor domainSaleor-Signature
- defines a signature to indicate that the request is verifiable
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 fromhttp(s)://<your-backend-domain>/.well-known/jwks.json
(available since Saleor 3.5, before 3.5 the payload from webhooks withoutsecretKey
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>
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:
Name | Description |
---|---|
ANY_EVENTS | Receive webhooks when any of the available events is triggered (wildcard event). |
ADDRESS_CREATED | A new address is created. |
ADDRESS_UPDATED | An address is updated. |
ADDRESS_DELETED | An address is deleted. |
APP_INSTALLED | A new App installed. |
APP_UPDATED | An App is updated. |
APP_DELETED | An App is deleted. |
APP_STATUS_CHANGED | An App status is changed. |
ATTRIBUTE_CREATED | A new attribute created. |
ATTRIBUTE_UPDATED | An attribute updated. |
ATTRIBUTE_DELETED | An attribute deleted. |
CATEGORY_CREATED | A new category created. |
CATEGORY_UPDATED | A category is updated. |
CATEGORY_DELETED | A category is deleted. |
CHANNEL_CREATED | A new channel is created. |
CHANNEL_UPDATED | A channel is updated. |
CHANNEL_DELETED | A channel is deleted. |
CHANNEL_STATUS_CHANGED | A channel status is changed. |
GIFT_CARD_CREATED | A new gift card is created. |
GIFT_CARD_UPDATED | A gift card is updated. |
GIFT_CARD_DELETED | A gift card is deleted. |
GIFT_CARD_STATUS_CHANGED | A gift card status is changed. |
CHECKOUT_CREATED | A new checkout is created. |
CHECKOUT_UPDATED | A checkout is updated. Also triggers for all updates related to a checkout. |
CUSTOMER_CREATED | A new customer account is created. |
CUSTOMER_UPDATED | A customer account is updated. |
CUSTOMER_DELETED | A customer account is deleted. |
FULFILLMENT_CREATED | A new fulfillment is created. |
INVOICE_DELETED | An invoice is deleted. |
INVOICE_REQUESTED | An invoice for an order is requested. At this stage number , created and external_url fields are nulls. |
INVOICE_SENT | An invoice is sent. |
MENU_CREATED | A new menu is created. |
MENU_UPDATED | A menu is updated. |
MENU_DELETED | A menu is deleted. |
MENU_ITEM_CREATED | A new menu item is created. |
MENU_ITEM_UPDATED | A menu item is updated |
MENU_ITEM_DELETED | A menu item is deleted. |
ORDER_CANCELLED | An order is cancelled. |
ORDER_CONFIRMED | An 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_CREATED | A new order is placed. |
ORDER_FULFILLED | An order is fulfilled. |
ORDER_FULLY_PAID | Payment is made and an order is fully paid. |
ORDER_UPDATED | An order is updated; triggered for all changes related to an order; covers all other order webhooks, except for ORDER_CREATED . |
SALE_CREATED | A new sale is created. |
SALE_UPDATED | A sale is updated. |
SALE_DELETED | A sale is deleted. |
SALE_TOGGLE | A sale is starting or ending. |
PAGE_CREATED | A new page is created. |
PAGE_DELETED | A page is deleted. |
PAGE_UPDATED | A page is updated. |
PAGE_TYPE_CREATED | A new page type is created. |
PAGE_TYPE_UPDATED | A page type is updated. |
PAGE_TYPE_DELETED | A page type is deleted. |
COLLECTION_CREATED | A new collection is created. |
COLLECTION_DELETED | A collection is deleted. |
COLLECTION_UPDATED | A collection is updated. |
PRODUCT_CREATED | A new product is created. |
PRODUCT_DELETED | A product is deleted. |
PRODUCT_UPDATED | A product is updated. |
PRODUCT_VARIANT_CREATED | A new product variant is created. |
PRODUCT_VARIANT_DELETED | A product variant is deleted. |
PRODUCT_VARIANT_UPDATED | A product variant is updated. |
NOTIFY | Trigger notification to user. |
TRANSACTION_ACTION_REQUEST | An action request for a transaction. Called when the state of the transaction should be changed. See sample payload. |
TRANSLATION_CREATED | A new translation is created. |
TRANSLATION_UPDATED | A translation is updated. |
PRODUCT_VARIANT_OUT_OF_STOCK | A product variant is out of stock. |
PRODUCT_VARIANT_BACK_IN_STOCK | A product variant is back in stock. |
SHIPPING_PRICE_CREATED | A new shipping price created. |
SHIPPING_PRICE_UPDATED | A shipping price is updated. |
SHIPPING_PRICE_DELETED | A shipping price is deleted. |
SHIPPING_ZONE_CREATED | A new shipping zone created. |
SHIPPING_ZONE_UPDATED | A shipping zone is updated. |
SHIPPING_ZONE_DELETED | A shipping zone is deleted. |
STAFF_CREATED | A new staff user is created. |
STAFF_UPDATED | A staff user is updated. |
STAFF_DELETED | A staff user is deleted. |
VOUCHER_CREATED | A new voucher is created. |
VOUCHER_UPDATED | A voucher is updated. |
VOUCHER_DELETED | A voucher is deleted. |
WAREHOUSE_CRATED | A new warehouse is created. |
WAREHOUSE_UPDATED | A warehouse is updated. |
WAREHOUSE_DELETED | A 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 theAuthentication
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
}
}
}