//...everything we've already covered
//this defines the shape of the data that Stripe returns when we create/update a webhook
const WebhookDataSchema = z.object({
id: z.string(),
object: z.literal("webhook_endpoint"),
api_version: z.string().nullable(),
application: z.string().nullable(),
created: z.number(),
description: z.string().nullable(),
enabled_events: z.array(z.string()),
livemode: z.boolean(),
metadata: z.record(z.string()),
status: z.enum(["enabled", "disabled"]),
url: z.string(),
});
function createWebhookEventSource(
//the integration is used to register the webhook
integration: Stripe
// { connect?: boolean } comes through from the params in the ExternalSourceTrigger we defined above
): ExternalSource<Stripe, { connect?: boolean }, "HTTP", {}> {
return new ExternalSource("HTTP", {
//this needs to be unique in Trigger.dev
//there's only one stripe webhook endpoint (for all events), so we use "stripe.webhook"
id: "stripe.webhook",
//this is the schema for the params that come through from the ExternalSourceTrigger
schema: z.object({ connect: z.boolean().optional() }),
version: "0.1.0",
integration,
//if the key is the same then a webhook will be updated (with any new events added)
//if the key is different then a new webhook will be created
//in Stripe's case we can have webhooks with multiple events BUT not shared between connect and non-connect
key: (params) => `stripe.webhook${params.connect ? ".connect" : ""}`,
//the webookHandler is called when the webhook is received, this is the last step we'll cover
handler: webhookHandler,
//this function is called when the webhook is registered
register: async (event, io, ctx) => {
const { params, source: httpSource, options } = event;
//httpSource.data is the stored data about the existing webhooks that has the same key
//httpSource.data will be undefined if no webhook has been registered yet with the same key
const webhookData = WebhookDataSchema.safeParse(httpSource.data);
//this is the full list of events that we want to register (when we add more than just onPriceCreated)
const allEvents = Array.from(new Set([...options.event.desired, ...options.event.missing]));
const registeredOptions = {
event: allEvents,
};
//if there is already an active source (i.e. a webhook has been registered)
//and httpSource.data was parsed successfully
if (httpSource.active && webhookData.success) {
//there are no missing events, so we don't need to update the webhook
if (options.event.missing.length === 0) return;
//we want to update the existing webhook with the new events
//this uses the Task we created above
const updatedWebhook = await io.integration.webhookEndpoints.update("update-webhook", {
id: webhookData.data.id,
url: httpSource.url,
enabled_events: allEvents as unknown as WebhookEvents[],
});
//when registering new events, we need to return the data and the options
return {
data: WebhookDataSchema.parse(updatedWebhook),
options: registeredOptions,
};
}
//if there is no active source, or httpSource.data wasn't parsed successfully,
//but we might be able to add events to an existing webhook
const listResponse = await io.integration.webhookEndpoints.list("list-webhooks", {
limit: 100,
});
//if one of these webhooks has the URL we want, we can update it
const existingWebhook = listResponse.data.find((w) => w.url === httpSource.url);
if (existingWebhook) {
//add all the events to the webhook
const updatedWebhook = await io.integration.webhookEndpoints.update(
"update-found-webhook",
{
id: existingWebhook.id,
url: httpSource.url,
enabled_events: allEvents as unknown as WebhookEvents[],
disabled: false,
}
);
//return the data and the registered options
return {
data: WebhookDataSchema.parse(updatedWebhook),
options: registeredOptions,
};
}
//there are no matching webhooks, so we need to create a new one
const webhook = await io.integration.webhookEndpoints.create("create-webhook", {
url: httpSource.url,
enabled_events: allEvents as unknown as WebhookEvents[],
connect: params.connect,
});
//when creating a new webhook, we need to also return the secret that Stripe sends us
//the secret is used to validate the webhook payloads we receive
return {
data: WebhookDataSchema.parse(webhook),
secret: webhook.secret,
options: registeredOptions,
};
},
});
}