Working With Webhooks Programmatically
As part of catalogue maintenance, order fulfilment, subscription management, tenant copy, and other processes, you may want to subscribe to various events (such as order creation) so that you can respond to them in a constructive fashion. In Crystallize, you do this by setting up webhooks.
You can use either the Crystallize App or the PIM API to manage what Crystallize-related events you want to listen for and how Crystallize should respond (if at all) when they fire. To do so, you'll first need the requisite permissions for Webhooks (read, create, etc). Refer to our documentation on roles and permissions for more information.
Below, we’ll discuss how to get the most out of webhooks within your programming.
There are several advantages to webhooks. For one, you get automated event management without having to use polling, which is almost always wasteful. Perhaps the most important advantage to webhooks is that they enable true decoupling. For example: when you receive payment confirmation on your service API, at that moment, your implementation (code) is probably pushing the order to Crystallize. At the same time, you may also want to send an email to the customer. You could do it within the same implementation:
- Push order to Crystallize
- Send email
But by doing it this way, you end up coupling two tasks that ought to remain separate. Modifying either of these processes in the future may have negative impacts on the other. This violates the SOLID paradigm, making your code more complicated and harder to maintain.
Instead, you should remove pushing orders to Crystallize from your implementation and set up a webhook on onNewOrderCreated. Then, create a separate endpoint in charge of sending the email when the payload is received. This is a basic example, but if you combine it with the order pipeline and many other events that webhooks can listen on, you have the potential to make your code base really simple. Here’s an example of that from our Remix Furniture Boilerplate:
import { ActionFunction, json } from '@remix-run/node';
import { getContext } from '~/use-cases/http/utils';
import { getStoreFront } from '~/use-cases/storefront.server';
import sendOrderCreatedReceipt from '~/use-cases/user/sendOrderCreatedReceipt';
import { createMailer } from '~/use-cases/services.server';
export const action: ActionFunction = async ({ request }) => {
if (request.method !== 'POST') {
return json({ message: 'Method not allowed' }, 405);
}
const mailer = createMailer(`${process.env.MAILER_DSN}`);
const requestContext = getContext(request);
const { secret } = await getStoreFront(requestContext.host);
const payload = await request.json();
await sendOrderCreatedReceipt(mailer, secret.apiClient, payload.order.get);
return json({ success: true, payload }, 200);
};
Another good example of a webhook is to purge the HTTP cache when something has changed, as shown in this boilerplate example:
import { ActionFunction, json } from '@remix-run/node';
import { getContext } from '~/use-cases/http/utils';
import { getStoreFront } from '~/use-cases/storefront.server';
import purgeKeys from '~/use-cases/http/fastly/purgeKeys';
export const action: ActionFunction = async ({ request }) => {
const requestContext = getContext(request);
const { secret: storefront } = await getStoreFront(requestContext.host);
// we keep it simple for now and we purge all cache for the tenant identifier
const keys = [storefront.config.tenantIdentifier];
const keyPurged = await purgeKeys(keys);
return json({
message: `${Object.keys(keyPurged).length} key(s) soft purged.`,
keys: keyPurged,
});
};
Some other good use cases for webhooks include:
- Rebuilding a static website when content has changed
- Subscription management
- Managing digital product downloads
- Notifying customers of new products, blog posts, articles, etc.
Here are all of the Crystallize-related concerns and events to which you can subscribe:
- Create
- Update
- Delete
- Pipeline state change
- Pipeline removed
- Create
- Update
- Delete
- Create
- Update
- Publish
- Unpublish
- Stock Updated
- Delete
- Added to flow stage
- Deleted from flow stage
- Create
- Update
- Publish
- Delete
- Create
- Update
- Delete
- Create
- Update
- Delete
- Create
- Update
- Delete
- Create
- Update
- Delete
- Renew
- Cancel
- Copy Started
- Copy Finished
- Copy Failed
Webhook-related mutations and queries are contained within the PIM API. At our example repository, you’ll find sample GraphQL queries for creating a webhook and fetching a webhook. We’re constantly improving Crystallize, so always check the API Docs for the latest schema info.
When defining a webhook, you must specify a callback URL and HTTPS request method (supported types are GET, POST, PUT, PATCH, or DELETE). While your implementation is still in the testing stage, you can set up something like Webhook.site or ngrok to use as a test URL.
You can optionally specify headers and a payload using a GraphQL query (mutations aren’t allowed in the payload). Note that header names will be converted and stored as lowercase (ex. Hello-Test will become hello-test). If you don’t specify a payload, the webhook endpoint will still have a default JSON payload containing all of the parameters available to such a query. If you specify a custom payload and also want the contents of the default payload, you can set the preserveDefaultPayload flag to true. For improved performance, it’s recommended to fetch all the data that your webhook endpoint may need in one go.
For each webhook, Crystallize stores the last 50 invocations sent to the callback URL. For each invocation, you’re able to get the HTTP status code, start and end times, payload, and response body. Webhook requests that fail are not retried.
Deleting a webhook and its invocation history is not reversible, so proceed with care.
For added security, the requests sent by Crystallize webhooks are signed. It is your responsibility to verify these signatures.
Refer to the User Guide for more information on creating, editing, and deleting webhooks within the Crystallize App.