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.

2 min read

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.

Product updates for developers shipping with Designyff kits.

Purchased source may be used in personal, commercial, and client projects. Do not redistribute or resell the original source. Terms of Service

Designyff © 2026 | Starter kits for developers | Powered by  Tacko