Job posting and checkout
Build public plan selection, job submission, reusable billing, invoices, and checkout.
A
JBuild 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.publicJobSubmissionistrue.- 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
123456789101112// src/data/job-posting.tsimport 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
12345678910111213141516171819202122232425262728293031323334export 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.
Add a logo
123456const 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.