GHOST
GHOST3d ago

Razorpay

Hey Team, how can I integrate Razorpay payment gateway (a popular payment gateway solution for Indian transactions) into my Convex project? I am using Next.js for my front-end. I am stuck in the process
1 Reply
Draco
Draco3d ago
If it's anything like stripe/polar/lemonsqueezy/ all those stripe-based providers, you're probably gonna wanna register a webhook handler, and then you create a client for it in a file:
import env from "@/env";
import { Polar } from "@polar-sh/sdk";
import { action } from "./_generated/server";
import { ConvexError, v } from "convex/values";
import { tryCatch } from "@/lib/utils";
import { Checkout } from "@polar-sh/sdk/models/components/checkout.js";
import { getCurrentUserOrThrow } from "./auth";

export const polarClient = new Polar({
accessToken: env.POLAR_DEV_ACCESS_TOKEN, // Currently we want it to be in sandbox mode all the time, will add dev and prod key switching later
server: "sandbox",
});

export const getOrCreatePolarByExternalId = async (
userId: string,
name: string,
email: string
) => {
// Get Polar Customer if they exist

const polarCustomer = await tryCatch(
polarClient.customers.getExternal({
externalId: userId,
})
);
// Not gonna handle the error, just gonna create a new customer if they don't exist
let polarId = polarCustomer.data?.id ?? "";
// If they don't exist, create them
if (polarId.length === 0 || !polarCustomer.data) {
const customer = await tryCatch(
polarClient.customers.create({
name,
externalId: userId,
email,
})
);
console.log(customer);
if (customer.error) {
console.error(customer.error);
return "";
}
polarId = customer.data.id;
}
return polarId;
};
import env from "@/env";
import { Polar } from "@polar-sh/sdk";
import { action } from "./_generated/server";
import { ConvexError, v } from "convex/values";
import { tryCatch } from "@/lib/utils";
import { Checkout } from "@polar-sh/sdk/models/components/checkout.js";
import { getCurrentUserOrThrow } from "./auth";

export const polarClient = new Polar({
accessToken: env.POLAR_DEV_ACCESS_TOKEN, // Currently we want it to be in sandbox mode all the time, will add dev and prod key switching later
server: "sandbox",
});

export const getOrCreatePolarByExternalId = async (
userId: string,
name: string,
email: string
) => {
// Get Polar Customer if they exist

const polarCustomer = await tryCatch(
polarClient.customers.getExternal({
externalId: userId,
})
);
// Not gonna handle the error, just gonna create a new customer if they don't exist
let polarId = polarCustomer.data?.id ?? "";
// If they don't exist, create them
if (polarId.length === 0 || !polarCustomer.data) {
const customer = await tryCatch(
polarClient.customers.create({
name,
externalId: userId,
email,
})
);
console.log(customer);
if (customer.error) {
console.error(customer.error);
return "";
}
polarId = customer.data.id;
}
return polarId;
};
export const createCheckout = action({
args: {
products: v.array(v.string()),
},
handler: async (ctx, { products }) => {
const authUser = await getCurrentUserOrThrow(ctx);
if (!authUser) {
throw new ConvexError("User not found");
}
const checkout = await polarClient.checkouts.create({
products,
customerEmail: authUser.email ?? "",
customerName: authUser.name ?? "",
customerExternalId: authUser.id,
customerBillingAddress: {
country: "US",
},
successUrl: `${env.NEXT_PUBLIC_DOMAIN}/dashboard?success=true`,
});
return JSON.parse(JSON.stringify(checkout)) as Checkout;
},
});

export const getCustomerPortalLink = action({
args: {},
handler: async (ctx) => {
const authUser = await getCurrentUserOrThrow(ctx);
if (!authUser) {
throw new ConvexError("User not found");
}
const customerSession = await tryCatch(
polarClient.customerSessions.create({
customerExternalId: authUser.id,
})
);
if (customerSession.error) {
throw new ConvexError("Customer could not be created.");
}
return {
customerPortalUrl: customerSession.data?.customerPortalUrl,
};
},
});

export default polarClient;
export const createCheckout = action({
args: {
products: v.array(v.string()),
},
handler: async (ctx, { products }) => {
const authUser = await getCurrentUserOrThrow(ctx);
if (!authUser) {
throw new ConvexError("User not found");
}
const checkout = await polarClient.checkouts.create({
products,
customerEmail: authUser.email ?? "",
customerName: authUser.name ?? "",
customerExternalId: authUser.id,
customerBillingAddress: {
country: "US",
},
successUrl: `${env.NEXT_PUBLIC_DOMAIN}/dashboard?success=true`,
});
return JSON.parse(JSON.stringify(checkout)) as Checkout;
},
});

export const getCustomerPortalLink = action({
args: {},
handler: async (ctx) => {
const authUser = await getCurrentUserOrThrow(ctx);
if (!authUser) {
throw new ConvexError("User not found");
}
const customerSession = await tryCatch(
polarClient.customerSessions.create({
customerExternalId: authUser.id,
})
);
if (customerSession.error) {
throw new ConvexError("Customer could not be created.");
}
return {
customerPortalUrl: customerSession.data?.customerPortalUrl,
};
},
});

export default polarClient;
This is how I did it for polar Polar does have a convex plugin, but it's honestly not very good and lacks alot of features but now I've moved to the better-auth polar plugin, that's quite a bit better Anyways, then after the client, you're going to want to register a webhook as per the docs for razorpay
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import env from "@/env";
import {
validateEvent,
WebhookVerificationError,
} from "@polar-sh/sdk/webhooks";
const http = httpRouter();

/* <!-------------------------------- POLAR WEBHOOK HANDLING --------------------------------> */

http.route({
path: "/polar-webhook",
method: "POST",
handler: httpAction(async (ctx, request) => {
const rawBody = await request.text();

try {
const event = await validateEvent(
rawBody, //raw body
Object.fromEntries(request.headers.entries()), //headers
env.POLAR_WEBHOOK_SECRET //secret
);
console.log("Polar webhook event", event);

switch (event.type) {
case "checkout.created": {
// Log in posthog n stuff for abandoned checkout metrics n stuff
console.log("Checkout created", event.data);
break;
}
case "checkout.updated": {
// Log in posthog n stuff for completed checkout metrics n stuff
console.log("Checkout completed", event.data);
break;
}
default:
console.log("Ignored Polar webhook event", event.type);
}
} catch (e) {
if (e instanceof WebhookVerificationError) {
return new Response("Error occurred", { status: 403 });
}
console.error("Error validating Polar webhook", e);
return new Response("Error occurred", { status: 400 });
}
return new Response(null, { status: 200 });
}),
});

/* <!-------------------------------- POlAR WEBHOOK END --------------------------------> */

export default http;
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import env from "@/env";
import {
validateEvent,
WebhookVerificationError,
} from "@polar-sh/sdk/webhooks";
const http = httpRouter();

/* <!-------------------------------- POLAR WEBHOOK HANDLING --------------------------------> */

http.route({
path: "/polar-webhook",
method: "POST",
handler: httpAction(async (ctx, request) => {
const rawBody = await request.text();

try {
const event = await validateEvent(
rawBody, //raw body
Object.fromEntries(request.headers.entries()), //headers
env.POLAR_WEBHOOK_SECRET //secret
);
console.log("Polar webhook event", event);

switch (event.type) {
case "checkout.created": {
// Log in posthog n stuff for abandoned checkout metrics n stuff
console.log("Checkout created", event.data);
break;
}
case "checkout.updated": {
// Log in posthog n stuff for completed checkout metrics n stuff
console.log("Checkout completed", event.data);
break;
}
default:
console.log("Ignored Polar webhook event", event.type);
}
} catch (e) {
if (e instanceof WebhookVerificationError) {
return new Response("Error occurred", { status: 403 });
}
console.error("Error validating Polar webhook", e);
return new Response("Error occurred", { status: 400 });
}
return new Response(null, { status: 200 });
}),
});

/* <!-------------------------------- POlAR WEBHOOK END --------------------------------> */

export default http;
In polar that would look somethin glike this If at any point your convex console tells you you need a nodejs runtime for a certain mutation/query/action, then just move that one specific function to a different file, usually i'd do smth like polar.ts -> polarNode.ts and then add a "use node"; directive at the top of the file Everything else, you can use the library as you would normally without convex

Did you find this page helpful?