Applications
Build guest and signed-in apply flows, resume upload, history, editing, and withdrawal.
A
JBuild a native application flow without hard-coding one universal form. Cavuno decides whether guests may apply and validates the live requirements; your frontend collects the values and renders the returned application state.
Prerequisites
- A published job loaded with
board.jobs.retrieve. - Candidate sessions if the board requires sign-in.
- A file input if you accept application resumes.
board.jobs.apply supports optional authentication. A signed-in candidate may omit name and email; a guest supplies them.
Submit an application
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950// src/data/applications.tsimport type { FetchOptions } from '@cavuno/board';import { board } from '../lib/board';function session(accessToken: string): FetchOptions {return {cache: 'no-store',headers: { authorization: `Bearer ${accessToken}` },};}function capableSession(accessToken: string): FetchOptions {return {cache: 'no-store',headers: {authorization: `Bearer ${accessToken}`,'x-cavuno-board-capabilities': 'apply-gateway-v1',},};}export async function applyAsGuest(jobSlug: string,input: { name: string; email: string; coverNote?: string; resume?: Blob },) {const application = await board.jobs.apply(jobSlug, {name: input.name,email: input.email,coverNote: input.coverNote,});if (!input.resume) return application;return board.jobs.uploadApplicationResume(jobSlug, input.resume, {applicationId: application.id,});}export async function applyAsCandidate(accessToken: string,jobSlug: string,coverNote?: string,) {return board.jobs.apply(jobSlug,{ coverNote },session(accessToken),);}
Apply is idempotent: a repeated submission returns the existing application. Do not show a second success state or create a local duplicate.
Handle country-gated native Apply
When a capable starter receives applyAction: 'gateway_native', prepare the signed-in Candidate's approval from your server with board.jobs.prepareApplyApproval. Keep its sessionKey in an HTTP-only board cookie; do not derive it from Profile data.
Load the job with x-cavuno-board-capabilities: apply-gateway-v1 in options.headers, and send the same capability on preparation and final Apply. The SDK deliberately does not add this header globally: it is an opt-in for starters that implement the complete browser-edge protocol.
The native receipt protocol is for signed-in Candidates. The current starter sends an anonymous visitor through sign-in before preparing gateway_native; guest-native API submissions retain the legacy behavior and are not country-gated by this protocol.
If the plan is approval_required, have the Candidate browser POST directly to approvalUrl with credentials: 'omit' and no body. The exact board origin is checked by Cavuno. Submit the returned receipt ID and the same server-owned key with the native application:
123456789101112131415161718192021const plan = await board.jobs.prepareApplyApproval(jobSlug,{ sessionKey },capableSession(accessToken),);let approvalReceipt: string | undefined;if (plan.kind === 'approval_required') {const response = await fetch(plan.approvalUrl, {method: 'POST',credentials: 'omit',});if (!response.ok) throw new Error('Apply is unavailable from this location');approvalReceipt = (await response.json()).id;}await board.jobs.apply(jobSlug,{ coverNote, approvalReceipt, approvalSessionKey: sessionKey },capableSession(accessToken),);
Do not send a country, IP, forwarding header, cookies, or an application body to approvalUrl. If the plan is not_required, continue with the ordinary native submission.
Build application history
12345678910111213141516171819202122232425262728export async function loadApplications(accessToken: string) {return board.me.applications.list({ limit: 20 },session(accessToken),);}export async function editApplication(accessToken: string,applicationId: string,coverNote: string,) {return board.me.applications.updateFacts(applicationId,{ coverNote },session(accessToken),);}export async function withdrawApplication(accessToken: string,applicationId: string,) {await board.me.applications.withdraw(applicationId,session(accessToken),);}
withdraw resolves with no body after the permanent deletion succeeds. Remove the row only after the promise resolves.
Expected result
A successful submission returns one Application with its server-owned status and facts. Signed-in history returns only the viewer’s applications; a completed withdrawal returns no response body.
Verify the result
- A guest application includes the returned
id, status, and candidate facts. - Resume upload uses the guest
application.id; signed-in uploads do not need it. - A repeat apply converges on the existing application.
- History lists only the signed-in candidate’s applications.
- Editing or withdrawing another user’s application is rejected by the API.
Errors and edge cases
- A board can require authentication even though the method accepts a guest body.
- Resume upload uses multipart data. Pass a
Blob; do not JSON-encode the file. myApplication(jobSlug)throws a typed 404 when the signed-in candidate has not applied.- Candidate facts are editable only while the application remains editable.
- Employer review belongs to
board.me.companies.applicants, behind an employer session.
Production cautions
- Disable the submit control while a request is in flight, but rely on server idempotency for retries.
- Do not log resumes, cover notes, names, or email addresses.
- Keep candidate and employer authorization paths separate.
- Display server-returned status values rather than maintaining a parallel client workflow.
Next, add recurring discovery with Job alerts.