Job posting namespace

Reference for public plans, job submission, logos, billing verification, and entitlements.

Use board.jobPosting to build the anonymous public employer posting funnel. These methods do not require an employer account. Check board.context().features.publicJobSubmission before exposing the funnel; Cavuno remains the authority for plan availability, billing credit, quotas, approval, invoicing, and publication.

Shared behavior

Every method returns the Board API wire body unchanged. A non-2xx response throws BoardApiError; inspect status, code, details, and requestId. The final FetchOptions argument passes abort signals, headers, and framework cache directives through to fetch.

board.jobPosting.plans

List public plans for the posting wizard.

ts
board.jobPosting.plans(
query?: { purpose?: 'job_posting' | 'talent_access' },
options?: FetchOptions,
): Promise<ListEnvelope<JobPostingPlan>>

purpose defaults to job_posting. Use talent_access only when you intentionally need that plan catalog. The list is not cursor-paginated.

ts
const { data: plans } = await board.jobPosting.plans({
purpose: 'job_posting',
});
for (const plan of plans) {
console.log(plan.id, plan.name, plan.prices);
}

Each JobPostingPlan includes id, name/description, kind, billing interval, purpose, ordering/recommendation, invoice settings, price rows in minor units with isActive, and feature rows. Pass the chosen plan’s id as submission.selectedPlan to create.

A missing/private board returns 404. This public GET is safe to retry after a transient network failure.

Related method: board.jobPosting.create.

board.jobPosting.create

Submit the complete public job-posting payload and branch on the returned status.

ts
board.jobPosting.create(
input: CreateJobPostingInput,
options?: FetchOptions,
): Promise<JobPostingResult>

CreateJobPostingInput.submission requires company/contact identity, title, description, employment and remote options, officeLocations, applicationUrl, and salaryRangeEnabled. It can also carry plan, salary, seniority, in-office, featured, remote-permit/timezone, and custom-field data. Top-level options include derived remote projections, logoUrl, selectedBilling, invoice billing details, and colorMode.

ts
import type { CreateJobPostingInput } from '@cavuno/board';
const input: CreateJobPostingInput = {
submission: {
companyName: 'Acme Robotics',
companyWebsite: 'https://acme.example',
contactName: 'Jane Lee',
contactEmail: 'jane@acme.example',
title: 'Robotics Engineer',
description: 'Build production robotics systems.',
employmentType: 'full_time',
remoteOption: 'remote',
officeLocations: [],
applicationUrl: 'https://acme.example/careers/robotics-engineer',
salaryRangeEnabled: false,
selectedPlan: 'plans_123',
},
};
const result = await board.jobPosting.create(input);
switch (result.status) {
case 'checkout':
console.log(result.checkoutUrl, result.jobId);
break;
case 'published':
console.log(result.jobId, result.jobSlug);
break;
case 'pending_approval':
case 'invoice_sent':
console.log(result.jobId);
break;
}

JobPostingResult has four successful branches:

statusFieldsNext action
checkoutcheckoutUrl, jobIdRedirect only to the URL returned by Cavuno.
publishedjobId, jobSlugLink to the published job.
pending_approvaljobIdShow the approval-pending state.
invoice_sentjobIdExplain the invoice and publication timing.

A missing/private board returns 404. A closed funnel, invalid plan/quota, or invalid custom field returns job_posting_rejected with status 422 rather than another result status. Per-IP throttling returns 429; wait before retrying. Do not blindly retry after an unknown response—the first request may already have created a job or checkout. Preserve a local draft and let the poster explicitly continue.

Related methods: board.jobPosting.plans, board.jobPosting.uploadLogo, and board.jobPosting.getBillingOptions.

board.jobPosting.uploadLogo

Upload and normalize a company logo for the posting flow.

ts
board.jobPosting.uploadLogo(
file: Blob,
options?: FetchOptions,
): Promise<JobPostingLogoResult>

Accepted formats are JPEG, PNG, WebP, and GIF up to 2 MB. The SDK creates multipart FormData; do not set the content-type header yourself because the runtime must add the boundary.

ts
const logo = await board.jobPosting.uploadLogo(logoFile);
console.log(logo.publicUrl);

The result is { object: 'job_posting_logo', publicUrl }. Pass publicUrl back as the top-level logoUrl in CreateJobPostingInput.

No file returns validation_bad_request with status 400. A missing board returns 404. A file over 2 MB returns media_payload_too_large with status 413. An unsupported or mismatched image returns media_unsupported_type with status 415. Per-IP throttling returns 429. Ask the user to change an invalid file; only retry 429 after waiting.

Related methods: board.jobPosting.fetchLogoByDomain and board.jobPosting.create.

board.jobPosting.fetchLogoByDomain

Look up a company logo by domain, store it, and return the same logo result as upload.

ts
board.jobPosting.fetchLogoByDomain(
domain: string,
options?: FetchOptions,
): Promise<JobPostingLogoResult>

Pass a domain such as acme.example, not an arbitrary search phrase.

ts
const logo = await board.jobPosting.fetchLogoByDomain('acme.example');
console.log(logo.publicUrl);

Malformed input returns validation_bad_request with status 400. No usable logo returns job_posting_logo_not_found with status 404; treat that as an optional missing image and offer upload or continue without a logo. Per-IP throttling returns 429. An unconfigured lookup provider returns job_posting_logo_lookup_unavailable with status 503. Back off before retrying 429/503 rather than looping automatically.

Related methods: board.jobPosting.uploadLogo and board.jobPosting.create.

board.jobPosting.checkBilling

Check whether an email already has billing credit on this board.

ts
board.jobPosting.checkBilling(
input: { email: string },
options?: FetchOptions,
): Promise<JobPostingBillingCheck>
ts
const billing = await board.jobPosting.checkBilling({
email: 'hiring@acme.example',
});
console.log(billing.hasActiveBilling);

The result is { object: 'job_posting_billing_check', hasActiveBilling }. false is a successful answer, not an error.

A missing/private board returns 404. Rate limiting returns 429; wait before retrying. Because this POST only reads billing state, retrying once after a transient network failure is safe.

Related methods: board.jobPosting.sendBillingVerification and board.jobPosting.checkSubscriptionEntitlements.

board.jobPosting.sendBillingVerification

Email a verification token before exposing billing-credit choices.

ts
board.jobPosting.sendBillingVerification(
input: { email: string },
options?: FetchOptions,
): Promise<JobPostingBillingVerification>
ts
const verification = await board.jobPosting.sendBillingVerification({
email: 'hiring@acme.example',
});
console.log(verification.success);

The result is { object: 'job_posting_billing_verification', success }. A missing/private board returns 404. The route has both the shared auth-bucket rate limit and a tighter email/IP throttle; 429 means wait before another user-triggered attempt. Do not automatically retry email delivery, because repeated attempts can send multiple messages.

Related method: board.jobPosting.getBillingOptions.

board.jobPosting.getBillingOptions

Exchange the emailed verification token for the credit options the poster may select.

ts
board.jobPosting.getBillingOptions(
input: { verificationToken: string },
options?: FetchOptions,
): Promise<JobPostingBillingOptions>
ts
const billing = await board.jobPosting.getBillingOptions({
verificationToken,
});
for (const option of billing.options) {
console.log(option.planName, option.jobsRemaining);
}

The result contains object: 'job_posting_billing_options' and options. Each option includes id, type, plan ID/name/kind, capacity rows, total and remaining job/featured allowances, and renewsAt. Use the chosen option to construct the top-level selectedBilling passed to create; do not infer credit from email identity alone.

An empty options array is successful. A missing/private board returns 404. Rate limiting returns 429. Retrying once after a transient network failure is safe; do not retry an unchanged token indefinitely.

Related methods: board.jobPosting.sendBillingVerification and board.jobPosting.create.

board.jobPosting.checkSubscriptionEntitlements

Read the active and featured allowances an email’s subscription grants for a plan.

ts
board.jobPosting.checkSubscriptionEntitlements(
input: { email: string; planId: string },
options?: FetchOptions,
): Promise<JobPostingSubscriptionEntitlements>
ts
const entitlements = await board.jobPosting.checkSubscriptionEntitlements({
email: 'hiring@acme.example',
planId: 'plans_123',
});
if (entitlements.hasSubscription) {
console.log(entitlements.maxActiveRemaining);
console.log(entitlements.featuredSlotsRemaining);
}

The result includes hasSubscription, nullable subscriptionId, featured and active-job totals/remaining values, canFeature, and featureSelectionMode. When hasSubscription is false, the entitlement fields can be null; do not convert null into an allowance.

A missing/private board returns 404. Rate limiting returns 429. Because the method only reads entitlement state, retrying once after a transient network failure is safe.

Related methods: board.jobPosting.checkBilling, board.jobPosting.plans, and board.jobPosting.create.

See Build a job posting flow for the complete plan, billing, submission, and checkout sequence.