Jobs
Accepting paid job submissions with Stripe
Create a one-time Stripe Checkout Session for a job posting and publish the job only after payment_status is paid.
Charging for a job post is a one-time Checkout Session, not a subscription. The Job Board Starter creates the job first, sends the employer to Stripe, and publishes the job only after it can read a paid session. The board structure is in building a job board with Next.js.
Start Checkout with the job id
From the submit action, after the pending job exists:
const session = await stripe().checkout.sessions.create({
mode: "payment",
line_items: [{ price: process.env.STRIPE_JOB_PRICE_ID || "", quantity: 1 }],
success_url: `${process.env.APP_URL}/api/jobs/paid?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/submit`,
metadata: { jobId: job.id }
});
{CHECKOUT_SESSION_ID} is a literal Stripe replaces. mode is "payment". The price is a Dashboard price id in STRIPE_JOB_PRICE_ID, so you can change the amount without a deploy. metadata.jobId is your row id.
This uses the same Stripe Node SDK 17 call as Stripe Checkout with Next.js, with a different mode and success URL.
Confirm on the way back
src/app/api/jobs/paid/route.ts:
const sessionId = new URL(request.url).searchParams.get("session_id") || "";
const session = await stripe().checkout.sessions.retrieve(sessionId);
if (session.payment_status === "paid" && session.metadata?.jobId) {
await prisma.job.update({
where: { id: session.metadata.jobId },
data: { paid: true, status: "published" }
});
}
return NextResponse.redirect(new URL("/submit?paid=1", process.env.APP_URL));
retrieve talks to Stripe. Do not publish because the query string exists. payment_status === "paid" is the check. A canceled session never hits this URL if the employer uses the cancel button; if they do hit it unpaid, the if skips the update and the job stays pending.
The redirect to /submit?paid=1 is a message for the browser. It is not the authorization step.
What is missing if you stop here
This path runs when the browser returns. If the employer closes the tab on the success page, the job stays pending even though Stripe has the money. A checkout.session.completed webhook, as in Stripe webhooks with Next.js, should perform the same update. The job board kit’s paid route does not also register a webhook. Add one if a lost redirect would strand a paid post.
The handler does not check that session.amount_total matches your price. It trusts a paid session that carries jobId. Keep the price id on the session so a paid session is the one you created.
Want to skip the setup? The Job Board Starter already creates the Checkout Session and publishes the job when Stripe reports the session paid.