#Stripe callback
12 messages · Page 1 of 1 (latest)
Hi @lilac garden. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
To set up a callback in Convex Auth that creates a Stripe user after a user signs up, you should use the afterUserCreatedOrUpdated callback in your Convex Auth configuration. This callback is triggered after a user is created or updated during the sign-in process, and is the right place to perform additional actions such as creating a corresponding Stripe customer.
Here’s how you can set it up:
- In your Convex Auth config, add the afterUserCreatedOrUpdated callback.
- Inside this callback, use the Stripe SDK to create a customer using the user's information.
- Optionally, store the Stripe customer ID in your Convex database for future reference.
Example (pseudocode, adapt as needed):
import Stripe from "stripe";
import { convexAuth } from "@convex-dev/auth/server";
const stripe = new Stripe(process.env.STRIPE_KEY!, {
apiVersion: "2022-11-15",
});
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [/* your providers */],
callbacks: {
async afterUserCreatedOrUpdated(ctx, { userId, profile }) {
// Create a Stripe customer
const customer = await stripe.customers.create({
email: profile.email,
name: profile.name,
// Add any other relevant fields
});
// Store the Stripe customer ID in your users table
await ctx.db.patch(userId, { stripeCustomerId: customer.id });
},
},
});
- The afterUserCreatedOrUpdated callback is only called if you do not specify createOrUpdateUser. If you use createOrUpdateUser, you should perform the Stripe logic there instead.
- The profile object contains user information such as email and name, which you can use to create the Stripe customer.
For more details on the callback, see the documentation: Writing additional data during authentication and afterUserCreatedOrUpdated API reference.
If you need to handle Stripe webhooks (e.g., for payment confirmation), you should set up a Convex HTTP action as described in the Stripe integration guide, but for user creation, the afterUserCreatedOrUpdated callback is the right place to create the Stripe user record in response to sign-up events.
If you need a more detailed example or have a specific use case, let me know!
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@lilac garden Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Personally, I recommend just creating it on first checkout.
Yeah I ended up doing that. Can't call third parties during a mutation or whatever :/
Yes, you have to use an action.
<@&1228088700710293556> how do you handle webhooks?
Add this to your http.ts file.
import Stripe from "stripe";
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { internal } from "./_generated/api";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2025-06-30.basil",
});
const http = httpRouter();
// This is the public-facing endpoint that Stripe will call.
const handleStripeWebhook = httpAction(async (ctx, request) => {
console.log("Received http actions /stripe");
const signature = request.headers.get("stripe-signature")!;
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!webhookSecret) {
console.error(
"STRIPE_WEBHOOK_SECRET is not set in Convex environment variables.",
);
return new Response("Webhook secret not configured.", { status: 500 });
}
try {
const event = await stripe.webhooks.constructEventAsync(
await request.text(), // Raw request body
signature,
webhookSecret,
);
await ctx.scheduler.runAfter(0, internal.stripe.handleEvent, { event });
// Acknowledge receipt of the event
return new Response(null, { status: 200 });
} catch (err: unknown) {
console.error(
"Error verifying Stripe webhook signature:",
err instanceof Error ? err.message : String(err),
);
// Stripe expects a 400 response for signature-related errors.
return new Response(
`Webhook Error: ${err instanceof Error ? err.message : String(err)}`,
{ status: 400 },
);
}
});
http.route({
path: "/stripe",
method: "POST",
handler: handleStripeWebhook,
});
export default http;
And then in your stripe.ts file create an internal action handleEvent (which your webhook calls from http.ts)
I use approach similar to Theo's approach on handling events.
export const handleEvent = internalAction({
handler: async (ctx, { event }: { event: Stripe.Event }) => {
const stripeEvent = event;
try {
//...logs
switch (stripeEvent.type) {
case "checkout.session.completed": {
const session = stripeEvent.data.object;
const { clerkUserId, tier } = session.metadata ?? {};
if (!clerkUserId || !tier) {
throw new Error(
"Missing metadata: clerkUserId or tier from checkout session.",
);
}
// Sync the user's subscription state.
await scheduleSyncForUser(ctx, session, stripeEvent.type);
case "customer.subscription.created":
case "customer.subscription.updated":
case "customer.subscription.deleted":
case "customer.subscription.paused":
case "customer.subscription.resumed":
case "customer.subscription.pending_update_applied":
case "customer.subscription.pending_update_expired":
case "customer.subscription.trial_will_end":
case "invoice.paid":
case "invoice.payment_failed":
case "invoice.payment_action_required":
case "invoice.upcoming":
case "invoice.marked_uncollectible":
case "invoice.payment_succeeded":
case "payment_intent.succeeded":
case "payment_intent.payment_failed":
case "payment_intent.canceled":
await scheduleSyncForUser(
ctx,
stripeEvent.data.object,
stripeEvent.type,
);
break;
default: {
console.log(`[Webhook] Unhandled event type: ${stripeEvent.type}`);
}
}
} catch (error) {
//...
}
},
});
And this is the scheduleSyncForUser function:
// Helper function to extract Clerk User ID and schedule a database sync.
// This centralizes the logic for finding the user associated with a Stripe event.
const scheduleSyncForUser = async (
ctx: ActionCtx,
eventObject: {
customer?: string | Stripe.Customer | Stripe.DeletedCustomer | null;
metadata?: { clerkUserId?: string | null } | null;
},
eventType: string,
) => {
let clerkUserId: string | undefined | null;
// Attempt to get clerkUserId directly from the event object's metadata.
// This is common for Checkout Sessions and Subscriptions.
clerkUserId = eventObject.metadata?.clerkUserId;
// If not found, and a customer ID is available, fetch the customer
// to get the ID from their metadata. This is the fallback for most objects
// like Invoices and Payment Intents.
if (
!clerkUserId &&
eventObject.customer &&
typeof eventObject.customer === "string"
) {
try {
const customer = (await stripe.customers.retrieve(
eventObject.customer,
)) as Stripe.Customer;
clerkUserId = customer.metadata.clerkUserId;
} catch (error) {
console.error(
`[Webhook] Error retrieving customer ${eventObject.customer} for event ${eventType}:`,
error,
);
// We can't proceed without the customer object, so we stop here.
return;
}
}
if (!clerkUserId) {
const customerId =
typeof eventObject.customer === "string" ? eventObject.customer : "N/A";
console.error(
`[Webhook] Could not find clerkUserId for event ${eventType} (Customer ID: ${customerId}). Skipping sync.`,
);
return;
}
// Schedule the sync task to run immediately.
await ctx.scheduler.runAfter(0, internal.stripe.syncStripeDataToDb, {
subject: clerkUserId,
});
console.log(
`[Webhook] Scheduled sync for event ${eventType} for user ${clerkUserId}.`,
);
};