Applications

Build guest and signed-in apply flows, resume upload, history, editing, and withdrawal.

Build 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

ts
// src/data/applications.ts
import 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:

ts
const 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

ts
export 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.