Rich A
Rich A2mo ago

Advice on Webhooks and Polar

I need some advice. Polar claims to use the Standard Webhooks approach for validating webhook signatures as described here @https://docs.polar.sh/integrate/webhooks/delivery . However, I cannot find any good example or guidance on how to do this within Convex's standard http.ts file for managing http routes and responding to webhooks after the signature has been validated. In my http.ts has Clerk using svix, which is pretty straghtforward and works for both test and production environments. But how would I do this for Polar (or any other party that may send webhooks to my app using the Standard Webhooks approach as described in @https://www.standardwebhooks.com/ ? Does anyone have any good examples of this working? FYI, I have tried to use the https://www.convex.dev/components/polar but I had issues getting that to work as well. I would feel more confident if I knew that I could interact wth Polar directly and securely. Once I have that, I will take a another run at the Convex Polar Component I've tried lots of different things and looked for answers, so any steer would be appreciated.
Polar
Handle & monitor webhook deliveries - Polar
How to parse, validate and handle webhooks and monitor their deliveries on Polar
Convex
Polar
Add subscriptions and billing to your Convex app with Polar.
4 Replies
Convex Bot
Convex Bot2mo ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
dejeszio
dejeszio5w ago
Hi @Rich A , did you find your answer? Does this help, let me know! So what I did was that inside my httpAction I call another action, this action returns to me the verifiedData. So if the body and headers are valid using the polar sdk, the data is returned to me to further process, if not it will throw an error. Here is my implementation.
"use node";

import { v } from "convex/values";
import { internalAction } from "../_generated/server";
import { getEnvValOrThrow } from "../utils";
import { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks";
import { internal } from "../_generated/api";
import { convertDatesToISOString } from "./utils";

const polarWebhookSecret = getEnvValOrThrow("POLAR_WEBHOOK_SECRET");

export const verifyWebhookSignatureAndReturnEvent = internalAction({
args: {
headers: v.any(),
body: v.string(),
},
handler: async (ctx, args) => {
try {
const event = validateEvent(args.body, args.headers, polarWebhookSecret);

// Convert all dates in the event object to ISO strings
const sanitizedEvent = convertDatesToISOString(event);

return {
success: true as const,
data: sanitizedEvent
};
} catch (error) {
if (error instanceof WebhookVerificationError) {
await ctx.runMutation(internal.telegram.sendMessage.sendMessageMutation, { bot: 'error', message: `polar webhook verification failed due to known verification error` });
return {
success: false as const,
error: "Webhook verification failed"
};
}
await ctx.runMutation(internal.telegram.sendMessage.sendMessageMutation, { bot: 'error', message: `polar webhook verification failed for unknown reason` });
throw error;
}
}
})
"use node";

import { v } from "convex/values";
import { internalAction } from "../_generated/server";
import { getEnvValOrThrow } from "../utils";
import { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks";
import { internal } from "../_generated/api";
import { convertDatesToISOString } from "./utils";

const polarWebhookSecret = getEnvValOrThrow("POLAR_WEBHOOK_SECRET");

export const verifyWebhookSignatureAndReturnEvent = internalAction({
args: {
headers: v.any(),
body: v.string(),
},
handler: async (ctx, args) => {
try {
const event = validateEvent(args.body, args.headers, polarWebhookSecret);

// Convert all dates in the event object to ISO strings
const sanitizedEvent = convertDatesToISOString(event);

return {
success: true as const,
data: sanitizedEvent
};
} catch (error) {
if (error instanceof WebhookVerificationError) {
await ctx.runMutation(internal.telegram.sendMessage.sendMessageMutation, { bot: 'error', message: `polar webhook verification failed due to known verification error` });
return {
success: false as const,
error: "Webhook verification failed"
};
}
await ctx.runMutation(internal.telegram.sendMessage.sendMessageMutation, { bot: 'error', message: `polar webhook verification failed for unknown reason` });
throw error;
}
}
})
export const onPolarWebhookEventReceived = httpAction(async (ctx, request) => {
const body = await request.text();

// Polar wants a plain object, so we are creating one for them
const headers: Record<string, string> = {};
request.headers.forEach((value: string, key: string) => {
headers[key] = value;
});

// https://github.com/polarsource/polar-js -> provided by the docs to verify webhook signature
const verifiedData = await ctx.runAction(internal.polar.verifyWebhookSignature.verifyWebhookSignatureAndReturnEvent, { body: body, headers: headers });
if (verifiedData.success === false) {
return new Response("unauthorized", { status: 403 });
}

const event: PolarWebhookPayloadType = verifiedData.data;
if (!event) {
await ctx.runMutation(internal.telegram.sendMessage.sendMessageMutation, { bot: 'error', message: `no valid event data returned after verifying signature for polar webhooks` });
throw new Response('something went wrong with retrieving valid event data', { status: 400 });
}

const eventType = event.type;
if (!eventType) {
await ctx.runMutation(internal.telegram.sendMessage.sendMessageMutation, { bot: 'error', message: `no event type found in event data from polar webhook request. likely something is wrong with verifying the webhook.` });
throw new Response(null, { status: 400 });
}
...
})
export const onPolarWebhookEventReceived = httpAction(async (ctx, request) => {
const body = await request.text();

// Polar wants a plain object, so we are creating one for them
const headers: Record<string, string> = {};
request.headers.forEach((value: string, key: string) => {
headers[key] = value;
});

// https://github.com/polarsource/polar-js -> provided by the docs to verify webhook signature
const verifiedData = await ctx.runAction(internal.polar.verifyWebhookSignature.verifyWebhookSignatureAndReturnEvent, { body: body, headers: headers });
if (verifiedData.success === false) {
return new Response("unauthorized", { status: 403 });
}

const event: PolarWebhookPayloadType = verifiedData.data;
if (!event) {
await ctx.runMutation(internal.telegram.sendMessage.sendMessageMutation, { bot: 'error', message: `no valid event data returned after verifying signature for polar webhooks` });
throw new Response('something went wrong with retrieving valid event data', { status: 400 });
}

const eventType = event.type;
if (!eventType) {
await ctx.runMutation(internal.telegram.sendMessage.sendMessageMutation, { bot: 'error', message: `no event type found in event data from polar webhook request. likely something is wrong with verifying the webhook.` });
throw new Response(null, { status: 400 });
}
...
})
Rich A
Rich AOP5w ago
Thanks for the response! As it turned out (and I'm new to this) the issue was with validating signatures in Dev. It seems that Polar will intentially make that fail in dev (with a bypass) to prevent one from accidentally promoting insecure code to production. I kept insisting that signature validation should work in Dev. Once I figure that out, I just did the verification in production and it worked. So now I have a "sandbox" environment in dev that will bypass the validation, but my production envrionment will validate and require that it works. Live and learn! // Check for a DEV_MODE env var to bypass validation during development const isDev = process.env.DEV_MODE === 'true' || process.env.NODE_ENV === 'development'; console.log( ℹ️ process.env.DEV_MODE: ${process.env.DEV_MODE}); console.log( ℹ️ process.env.NODE_ENV: ${process.env.NODE_ENV}); try { // Validate the webhook using Polar's SDK event = validateEvent( body, headers, process.env.POLAR_WEBHOOK_SECRET ?? '', ); console.log('✅ Webhook signature validated successfully'); } catch (error) { // In development mode, allow invalid signatures but log the error console.warn('🔍 Webhook signature validation failed. If this is in dev, then this is expected.'); if (isDev) { console.log('🔍 DEV MODE: Bypassing invalid signature check'); // Parse the body as JSON for processing event = JSON.parse(body); } else { // In production, reject invalid signatures throw new Error('Invalid webhook signature in production'); }
dejeszio
dejeszio5w ago
haha, glad you figured it out! Nice!

Did you find this page helpful?