Skip to main content

Saleor Webhook Utilities

Apps are usually connected via webhooks - one App sends an HTTP request to another App, informing about some event or requesting some action to be performed.

To inform your App about events originating from Saleor, you need to expose a webhook handler, which Saleor will call with a POST request.

The App SDK provides a utility that abstracts connection details and auth, allowing developers to focus on business logic.

Depending on the type of the webhook, you can choose one of the classes:

  • SaleorAsyncWebhook
  • SaleorSyncWebhook

Common configuration

Both SaleorSyncWebhook and SaleorAsyncWebhook contain a similar API with few differences.

Constructing Webhook instance

In Next.js pages directory, create a page, e.g., pages/api/webhooks/order-created.ts. We recommend keeping the webhook type in the file name, which Next.js will resolve to route the path.

import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";

/**
* Default body parser must be turned off - the raw body is needed to verify the signature
*/
export const config = {
api: {
bodyParser: false,
},
};

/**
* To be type-safe, define payload from API. This should be imported from generated GraphQL code
*/
type OrderPayload = {
id: string;
};

export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderPayload>(
options
);
Expand ▼

For SaleorSyncWebhook, it will be similar. Create e.g., order-calculate-taxes.ts page and create a new instance:

import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next";

/**
* Default body parser must be turned off - the raw body is needed to verify the signature
*/
export const config = {
api: {
bodyParser: false,
},
};

/**
* To be type-safe, define payload from API. This should be imported from generated GraphQL code
*/
type CalculateTaxedPayload = {
// taxes payload from subscription
};

export const orderCalculateTaxesWebhook = new SaleorSyncWebhook<OrderPayload>(
options
);
Expand ▼

Configuring Webhook instance

options in the constructor must be specified. Here are all options:

type Options = {
/**
* Additional webhook name, optional.
*/
name?: string;
/**
* Path to webhook. It should represent a relative path from the base app URL. In Next.js, it will start with `api/`, e.g. `api/webhooks/order-created`.
*/
webhookPath: string;
/**
* Valid Async or Sync webhook. Constructor is statically typed, so only valid Sync/Async webhooks will be allowed
*/
event: Event;
/**
* Should event be active after installation, enabled by default
*/
isActive?: boolean;
/**
* APL instance - see docs/apl/md
*/
apl: APL;
/**
* Optional callback that allows to inject custom error handling and take control of the response
*/
onError?(
error: WebhookError | Error,
req: NextApiRequest,
res: NextApiResponse
): void;
/**
* Optional callback that allows formatting error messages. Useful to control how many details should be returned in the response
*/
formatErrorResponse?(
error: WebhookError | Error,
req: NextApiRequest,
res: NextApiResponse
): Promise<{
code: number;
body: object | string;
}>;
/**
* Required subscription query. Can be raw GraphQL string or the instance of query wrapped in `gql` tags
*/
query: string | ASTNode;
};
Expand ▼

Configuration examples

SaleorAsyncWebhook configuration example
// pages/api/webhooks/order-created.ts

/**
* To be type-safe, define payload from API. This should be imported from generated GraphQL code
*/
type OrderPayload = {
id: string;
};

/**
* Default body parser must be turned off - the raw body is needed to verify the signature
*/
export const config = {
api: {
bodyParser: false,
},
};

export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderPayload>({
name: "Order Created",
webhookPath: "api/webhooks/order-created",
event: "ORDER_CREATED",
isActive: true,
apl: require("../lib/apl"),
query: `
subscription {
event {
... on OrderCreated {
order {
id
}
}
}
}
`,
onError(error: WebhookError | Error) {
// Can be used to e.g. trace errors
sentry.captureError(error);
},
async formatErrorResponse(
error: WebhookError | Error,
req: NextApiRequest,
res: NextApiResponse
) {
return {
code: 400,
body: "My custom response",
};
},
});
Expand ▼
SyncAsyncWebhook configuration example
// pages/api/webhooks/order-created.ts

/**
* To be type-safe, define payload from API. This should be imported from generated GraphQL code
*/
type Payload = {
taxBase: {
currency: string;
};
};

/**
* Default body parser must be turned off - the raw body is needed to verify the signature
*/
export const config = {
api: {
bodyParser: false,
},
};

export const orderCalculateTaxesWebhook = new SaleorAsyncWebhook<Payload>({
name: "Order Calculate Taxes",
webhookPath: "api/webhooks/order-created",
event: "ORDER_CALCULATE_TAXES",
isActive: true,
apl: require("../lib/apl"),
query: `
subscription {
event {
... on CalculateTaxes {
taxBase {
currency
}
}
}
}
`,
onError(error: WebhookError | Error) {
sentry.captureError(error);
},
async formatErrorResponse(
error: WebhookError | Error,
req: NextApiRequest,
res: NextApiResponse
) {
return {
code: 400,
body: "My custom response",
};
},
});
Expand ▼

Resources

Extending app manifest

Webhooks are created in Saleor when the App is installed. Saleor uses AppManifest to get information about webhooks to create. SaleorSyncWebhook & SaleorAsyncWebhook utility can generate this manifest:

// pages/api/manifest

import { createManifestHandler } from "@saleor/app-sdk/handlers/next";

import { orderCreatedWebhook } from "./order-created.ts";

export default createManifestHandler({
manifestFactory({ appBaseUrl }) {
return {
/**
* Add one or more webhook manifests.
*/
webhooks: [orderCreatedWebhook.getWebhookManifest(appBaseUrl)],
// ...rest of your App's manifest
};
},
});

Now, try to read your manifest. In the default Next.js config, it will be GET localhost:3000/api/manifest. You should see the webhook configuration as part of the manifest response.

Creating webhook domain logic

Now, let's create a handler that will process webhook data. Let's go back to the handler file pages/api/webhooks/order-created.ts.

type OrderPayload = {
id: string;
};

export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderPayload>({
// ... your configuration
});

/**
* Handler has to be a default export so the Next.js will be able to use it
*/
export default orderCreatedWebhook.createHandler((req, res, context) => {
const { baseUrl, event, payload, authData } = context;

console.log(payload.id); // type is inferred

// Perform some domain logic

// End with status 200
return res.status(200).end();
});
Expand ▼

Typed sync webhook response

Sync webhooks need to return a response to Saleor so that the operation can be completed. To achieve that, SaleorAsyncWebhook injects an additional context field buildResponse. It infers even from the constructor and provides a typed factory:

const webhook = new SaleorAsyncWebhook({
event: "ORDER_CALCULATE_TAXES" /* ... rest of config */,
});

orderCreatedWebhook.createHandler((req, res, context) => {
return res.status(200).send(
context.buildResponse({
// Fields are typed here
})
);
});

query vs subscriptionQueryAst

Subscription query can be specified using plain string or as an ASTNode object created by gql tag.

If your project does not use any code generation for GraphQL operations, use the string. In case you are using GraphQL Code Generator, which we highly recommend, you should pass a subscription as GraphQL ASTNode:

/**
* Subscription query, you can define it in the `.ts` file. If you write operations in separate `.graphql` files, codegen will also export them in the generated file.
*/
export const ExampleProductUpdatedSubscription = gql`
${ProductUpdatedWebhookPayload}
subscription ExampleProductUpdated {
event {
fragment
ProductUpdatedWebhookPayload
on
ProductUpdated {
product {
id
name
}
}
}
}
`;

export const productUpdatedWebhook =
new SaleorAsyncWebhook<ProductUpdatedWebhookPayloadFragment>({
name: "Example product updated webhook",
webhookPath: "api/webhooks/saleor/product-updated",
event: "PRODUCT_UPDATED",
apl: saleorApp.apl,
query: ExampleProductUpdatedSubscription, // Or use plain string
});
Expand ▼

Saleor schema version inside context

From version 0.50.0 of @saleor-sdk have new field: schemaVersion inside context for createHandler:

export default orderCreatedWebhook.createHandler((req, res, context) => {
const { schemaVersion } = context;

console.log(schemaVersion);

// End with status 200
return res.status(200).end();
});

This field can be used to enable some feature based on the Saleor version. Before you use it make sure that your subscription have version field defined. If not schemaVersion will be null:

subscription {
event {
+ version
... on CalculateTaxes {
taxBase {
currency
}
}
}
}