Job posting and checkout

Build public plan selection, job submission, reusable billing, invoices, and checkout.

Build the public “post a job” funnel. It can complete as a published job, a pending approval, an invoice, or a hosted checkout. The discriminated response is the source of truth.

Prerequisites

  • board.context().features.publicJobSubmission is true.
  • A route that can render plan selection and the job form.
  • HTTPS success and cancellation destinations for paid checkout.
  • A server action or protected submission handler for the final write.

The public Board API key identifies the board; it is not a payment credential.

Load plans before the form

ts
// src/data/job-posting.ts
import type { CreateJobPostingInput } from '@cavuno/board';
import { board } from '../lib/board';
export async function loadJobPostingPlans() {
const context = await board.context();
if (!context.features.publicJobSubmission) return null;
const plans = await board.jobPosting.plans({ purpose: 'job_posting' });
return plans.data;
}

The selected plan’s id becomes submission.selectedPlan. Show active prices and invoice behavior from the returned plan instead of maintaining a duplicate pricing table.

Submit and branch on the result

ts
export async function submitRemoteJob(selectedPlan: string) {
const input: CreateJobPostingInput = {
submission: {
companyName: 'Acme',
companyWebsite: 'https://acme.example',
contactName: 'Ada Lovelace',
contactEmail: 'ada@acme.example',
title: 'Staff Engineer',
description: 'Build reliable systems for our customers.',
employmentType: 'full_time',
remoteOption: 'remote',
officeLocations: [],
applicationUrl: 'https://acme.example/careers/staff-engineer',
salaryRangeEnabled: false,
selectedPlan,
remoteWorkingPermits: [
{ type: 'worldwide', value: 'worldwide', label: 'Worldwide' },
],
},
};
const result = await board.jobPosting.create(input);
switch (result.status) {
case 'checkout':
return { redirectTo: result.checkoutUrl } as const;
case 'published':
return { jobSlug: result.jobSlug } as const;
case 'pending_approval':
return { message: 'Your job is awaiting approval.' } as const;
case 'invoice_sent':
return { message: 'Your invoice has been sent.' } as const;
}
}

Redirect only to checkoutUrl returned by Cavuno. A return to your site is not proof that payment succeeded; the payment webhook controls publication.

Reuse existing billing

First call checkBilling({ email }). If hasActiveBilling is true, send a verification email, collect the token, then call getBillingOptions({ verificationToken }). A selected option maps to the top-level selectedBilling object on create.

Do not reveal billing options from an email address alone. The verification token is the authorization step.

ts
const logo = await board.jobPosting.uploadLogo(file);
const inputWithLogo: CreateJobPostingInput = {
...input,
logoUrl: logo.publicUrl,
};

Logo uploads accept JPEG, PNG, WebP, or GIF files up to 2 MB. fetchLogoByDomain(domain) is optional; a typed job_posting_logo_not_found error means the user should continue without an automatically discovered logo.

Expected result

Submission returns exactly one of four outcomes: a checkout URL, a published job slug, pending approval, or an invoice sent state. Validation and rejected submissions throw a typed error instead.

Verify the result

  • The route disappears when public submission is disabled.
  • Plan IDs and prices come from jobPosting.plans.
  • Each result status renders a distinct completion state.
  • Checkout uses only the returned URL.
  • A completed payment eventually produces a published job through server state, not client inference.

Errors and edge cases

  • Rejected submissions throw BoardApiError; they do not return a fifth status.
  • On-site and hybrid jobs need valid office location input. Invalid geocoding must be corrected, not silently dropped.
  • Salary values require a currency and timeframe when salary range is enabled.
  • Subscription feature entitlements can constrain featured slots and active jobs.
  • Invoice plans can require billing address, tax, or board-defined detail fields.

Production cautions

  • Keep contact and invoice fields out of analytics payloads and logs.
  • Validate URLs and file size before submission for fast feedback, then show server validation exactly.
  • Make completion pages safe to reload.
  • Re-read job state after checkout or invoice completion.

Next, add candidate access purchases with Candidate paywalls.