Me namespace
Reference for the signed-in user, candidate profile, employer tools, alerts, applications, messaging, and access.
A
Jboard.me contains the candidate and employer operations performed as the current board user. Token-authorized exceptions (confirmEmailChange, notification unsubscribe, work-email confirm) do not require a bearer; every other method does.
Do not put these responses in a shared public cache. In SSR applications, keep a module-scoped nostore client and pass the current request’s authorization in the method’s final FetchOptions argument.
123456const privateOptions = {cache: 'no-store' as const,headers: { authorization: `Bearer ${accessToken}` },};const user = await board.me.retrieve(undefined, privateOptions);
The undefined preserves the empty query slot in methods shaped as (query?, options?). Methods without a query slot accept options directly.
Identity
| Method and signature | Returns | Behavior |
|---|---|---|
board.me.retrieve(query?: Record<string, never>, options?: FetchOptions) | Promise<BoardUser> | Returns the authenticated user’s board identity, including hasPassword. |
board.me.delete(options?: FetchOptions) | Promise<void> | Permanently deletes the account and its dependent profile, collections, saved jobs, alerts, avatar, and resume data. |
board.me.updatePassword(body: UpdatePasswordBody, options?: FetchOptions) | Promise<BoardAuthSession> | Changes the password and persists the returned bearer pair. Passwordless accounts return no_password. |
board.me.requestEmailChange(body: RequestEmailChangeBody, options?: FetchOptions) | Promise<void> | Mails a verification link to the new address. The email swaps only after confirm. |
board.me.confirmEmailChange(body: ConfirmEmailChangeBody, options?: FetchOptions) | Promise<void> | Consumes the email-change token. No session required. |
board.me.acceptInvite(body: AcceptCompanyMemberInviteBody, options?: FetchOptions) | Promise<CompanyMemberInviteAcceptance> | Accepts a company member invite for the signed-in board user. Session-gated. |
BoardUser.hasPassword is false for magic-link and OAuth-only accounts. Offer change-password only when it is true. Passwordless accounts should call board.auth.forgotPassword (the existing reset email) rather than updatePassword, which returns no_password.
Account deletion is synchronous, irreversible, and resolves after a 204 response. Do not present it as a reversible sign-out action. updatePassword keeps the caller signed in; every other session dies. Email change is verify-then-switch: requestEmailChange mails the new address and can return same_email or email_taken; confirmEmailChange consumes the token with no session.
Candidate profile
Profile updates use merge-patch semantics: omitted fields remain unchanged. Collection create/update methods return the affected entity; delete methods resolve void.
| Method and signature | Returns |
|---|---|
board.me.profile.retrieve(query?: Record<string, never>, options?: FetchOptions) | Promise<CandidateProfile> |
board.me.profile.update(body: UpdateCandidateProfileBody, query?: Record<string, never>, options?: FetchOptions) | Promise<CandidateProfile> |
board.me.profile.handleAvailable(handle: string, options?: FetchOptions) | Promise<HandleAvailability> |
board.me.profile.listExperience(options?: FetchOptions) | Promise<ListEnvelope<CandidateExperience>> |
board.me.profile.createExperience(body: CreateExperienceBody, options?: FetchOptions) | Promise<CandidateExperience> |
board.me.profile.updateExperience(id: string, body: UpdateExperienceBody, options?: FetchOptions) | Promise<CandidateExperience> |
board.me.profile.deleteExperience(id: string, options?: FetchOptions) | Promise<void> |
board.me.profile.listEducation(options?: FetchOptions) | Promise<ListEnvelope<CandidateEducation>> |
board.me.profile.createEducation(body: CreateEducationBody, options?: FetchOptions) | Promise<CandidateEducation> |
board.me.profile.updateEducation(id: string, body: UpdateEducationBody, options?: FetchOptions) | Promise<CandidateEducation> |
board.me.profile.deleteEducation(id: string, options?: FetchOptions) | Promise<void> |
board.me.profile.listSkills(options?: FetchOptions) | Promise<ListEnvelope<CandidateSkill>> |
board.me.profile.updateSkills(body: UpdateSkillsBody, options?: FetchOptions) | Promise<ListEnvelope<CandidateSkill>> |
board.me.profile.listLanguages(options?: FetchOptions) | Promise<ListEnvelope<CandidateLanguage>> |
board.me.profile.updateLanguages(body: UpdateLanguagesBody, options?: FetchOptions) | Promise<ListEnvelope<CandidateLanguage>> |
board.me.profile.uploadAvatar(file: Blob, options?: FetchOptions) | Promise<CandidateAvatar> |
handleAvailable is advisory. A later profile.update rechecks the handle and can still throw candidate_handle_taken after another user claims it.
updateSkills and updateLanguages replace their ordered sets rather than merging individual entries. uploadAvatar accepts JPEG, PNG, or WebP up to 5 MB and builds the multipart body for you.
1234567891011121314151617181920const profile = await board.me.profile.update({ headline: 'Staff engineer' },undefined,privateOptions,);const experience = await board.me.profile.createExperience({companyName: 'Analytical Engines',title: 'Programmer',startDate: '2024-01',},privateOptions,);await board.me.profile.updateExperience(experience.id,{ title: 'Senior programmer' },privateOptions,);
Use the installed TypeScript types for the complete optional profile and collection fields; they are generated from the same API schemas as the SDK.
Notification preferences
| Method and signature | Returns | Authentication |
|---|---|---|
board.me.notificationPreferences.retrieve(options?: FetchOptions) | Promise<ListEnvelope<NotificationPreference>> | Bearer required. |
board.me.notificationPreferences.update(body: UpdateNotificationPreferenceBody, options?: FetchOptions) | Promise<ListEnvelope<NotificationPreference>> | Bearer required; returns the complete updated set. |
board.me.notificationPreferences.unsubscribeWithToken(body: UnsubscribeBody, options?: FetchOptions) | Promise<void> | The signed email token authorizes the request; no session required. |
unsubscribeWithToken is intended for one-click email links and resolves after a 204 response.
Channels are messageEmails, applicationEmails, and recommendedJobEmails. The first two default subscribed when no row exists. recommendedJobEmails is explicit opt-in: a missing row means unsubscribed. Each item includes subscribed and additive waitlisted. subscribed: true means they asked, including while queued; they receive mail only when subscribed is true and waitlisted is false. PUT at the Email subscribers cap waitlists instead of failing.
Marketing consent
Marketing consent is a property of the signed-in board user, separate from job alerts and notification preferences. The wording someone agrees to is yours — render your own disclosure copy beside any control that calls these methods; the API records the decision, never the prose. These helpers derive the person from the verified bearer and cannot target another email.
| Method and signature | Returns | Behavior |
|---|---|---|
board.me.marketingConsent.retrieve(options?: FetchOptions) | Promise<MarketingConsent | null> | Reads the current decision; null means none was ever recorded. |
board.me.marketingConsent.grant(options?: FetchOptions) | Promise<MarketingConsent> | Records consent. Call only from a surface that displayed your disclosure wording. Idempotent. |
board.me.marketingConsent.withdraw(options?: FetchOptions) | Promise<MarketingConsent> | Withdraws immediately and idempotently; repeated calls do not add another revision. |
Leave any checkbox unticked by default and call nothing when it stays unticked — absence of a record means no consent, never a default. Withdrawal is at least as direct as grant and stays effective while any downstream webhook delivery is pending.
Employer companies and claims
A board user becomes an employer by opening or receiving a company membership. An approved membership is required for company editing, job management, billing options, and the applicant pipeline.
| Method and signature | Returns |
|---|---|
board.me.companies.search(query: EmployerCompanySearchQuery, options?: FetchOptions) | Promise<ListEnvelope<ClaimableCompany>> |
board.me.companies.list(options?: FetchOptions) | Promise<ListEnvelope<CompanyMembership>> |
board.me.companies.create(body: CreateCompanyBody, options?: FetchOptions) | Promise<CompanyMembership> |
board.me.companies.claim(slug: string, options?: FetchOptions) | Promise<CompanyMembership> |
board.me.companies.cancelClaim(slug: string, options?: FetchOptions) | Promise<void> |
board.me.companies.retrieve(slug: string, options?: FetchOptions) | Promise<EmployerCompany> |
board.me.companies.update(slug: string, body: UpdateEmployerCompanyBody, options?: FetchOptions) | Promise<EmployerCompany> |
board.me.companies.delete(slug: string, options?: FetchOptions) | Promise<void> |
board.me.companies.listMembers(slug: string, options?: FetchOptions) | Promise<ListEnvelope<CompanyMember>> |
board.me.companies.updateMemberRole(slug: string, memberId: string, body: UpdateCompanyMemberRoleBody, options?: FetchOptions) | Promise<void> |
board.me.companies.removeMember(slug: string, memberId: string, options?: FetchOptions) | Promise<void> |
board.me.companies.leave(slug: string, options?: FetchOptions) | Promise<void> |
board.me.companies.listInvites(slug: string, options?: FetchOptions) | Promise<ListEnvelope<CompanyMemberInvite>> |
board.me.companies.createInvite(slug: string, body: CreateCompanyMemberInviteBody, options?: FetchOptions) | Promise<CompanyMemberInvite> |
board.me.companies.revokeInvite(slug: string, inviteId: string, options?: FetchOptions) | Promise<void> |
board.me.companies.uploadLogo(slug: string, file: Blob, options?: FetchOptions) | Promise<EmployerCompany> |
board.me.companies.workEmail.verify(slug: string, body: SendWorkEmailBody, options?: FetchOptions) | Promise<CompanyMembership> |
board.me.companies.workEmail.confirm(slug: string, body: ConfirmWorkEmailBody, options?: FetchOptions) | Promise<CompanyMembership> |
board.me.talentAccess.retrieve(options?: FetchOptions) | Promise<TalentAccess> |
companies.search requires { q } and accepts limit from 1–50. create can adopt an existing same-domain company. claim can return an approved membership immediately on an email-domain match; otherwise it remains pending.
companies.retrieve returns my full editable company profile (summary, socials, logo, description) — the read half of update, for pre-populating an edit form. companies.uploadLogo uploads and attaches a company logo (JPEG/PNG/WebP/GIF, ≤2 MB) in a single multipart POST and returns the updated company with its new logoUrl. Both require an approved membership.
companies.delete is admin-gated and returns company_deletion_disabled when the board operator has turned employer company deletion off. listMembers is visible to any approved member and includes role (admin or member) plus approvedBy join provenance (owner_creation, domain_match, work_email_verification, admin, or invite). updateMemberRole and removeMember are admin-only; demoting or removing the last admin returns last_admin. leave deletes the caller’s own membership; leaving as the last admin also returns last_admin. A company always has at least one admin. listInvites is visible to any approved member. createInvite and revokeInvite are admin-only; invites expire after 7 days; duplicate members or pending invites return already_member / already_invited. acceptInvite requires a bearer session whose email matches the invite (invite_email_mismatch otherwise) and an employer-role account (candidate_role for candidates).
workEmail.verify sends a 24-hour verification link for a pending claim. workEmail.confirm uses the token in that link as authorization and does not require a bearer session.
talentAccess.retrieve answers "does this viewer currently have talent access?" for talent-surface CTAs — an approved employer with an active talent subscription or credit pack (or any approved employer on a board without a talent paywall) gets hasTalentAccess: true; candidates get false. Never cache it in a shared cache.
Employer jobs and billing
Every job method is scoped by company slug. New employer jobs are held drafts. publish only republishes a job still inside its paid window; a held or expired draft can require checkout.
| Method and signature | Returns |
|---|---|
board.me.companies.jobs.list(slug: string, query?: EmployerJobsListQuery, options?: FetchOptions) | Promise<ListEnvelope<EmployerJobSummary>> |
board.me.companies.jobs.retrieve(slug: string, id: string, options?: FetchOptions) | Promise<EmployerJob> |
board.me.companies.jobs.create(slug: string, body: CreateEmployerJobBody, options?: FetchOptions) | Promise<EmployerJob> |
board.me.companies.jobs.update(slug: string, id: string, body: UpdateEmployerJobBody, options?: FetchOptions) | Promise<EmployerJob> |
board.me.companies.jobs.delete(slug: string, id: string, options?: FetchOptions) | Promise<void> |
board.me.companies.jobs.publish(slug: string, id: string, options?: FetchOptions) | Promise<EmployerJob> |
board.me.companies.jobs.unpublish(slug: string, id: string, options?: FetchOptions) | Promise<EmployerJob> |
board.me.companies.jobs.checkout(slug: string, id: string, body: EmployerCheckoutBody, options?: FetchOptions) | Promise<EmployerCheckout> |
board.me.companies.billingOptions.list(slug: string, options?: FetchOptions) | Promise<ListEnvelope<EmployerBillingOption>> |
board.me.companies.jobStats.retrieve(slug: string, options?: FetchOptions) | Promise<ListEnvelope<EmployerJobStat>> |
board.me.companies.jobStats.timeseries(slug: string, query?: EmployerJobStatsTimeseriesQuery, options?: FetchOptions) | Promise<ListEnvelope<EmployerJobStatsPoint>> |
board.me.companies.profileStats.retrieve(slug: string, options?: FetchOptions) | Promise<EmployerProfileStats> |
board.me.companies.profileStats.timeseries(slug: string, query?: EmployerProfileViewsTimeseriesQuery, options?: FetchOptions) | Promise<ListEnvelope<EmployerProfileViewsPoint>> |
checkout can resolve with status: 'checkout' and a hosted payment URL, status: 'published' when a free/bundle/subscription option publishes immediately, or status: 'invoice_sent'. Branch on the returned status instead of assuming every call needs a redirect.
1234567891011121314151617181920const draft = await board.me.companies.jobs.create('analytical-engines',{title: 'Staff engineer',description: 'Build systems for a growing team.',applicationUrl: 'https://example.com/apply',},privateOptions,);const result = await board.me.companies.jobs.checkout('analytical-engines',draft.id,{ billing: { type: 'new', planId } },privateOptions,);if (result.status === 'checkout' && result.checkoutUrl) {window.location.assign(result.checkoutUrl);}
jobStats.retrieve returns one EmployerJobStat per company job — views, applyClicks, and applications (null for an external-apply job, which has no native pipeline) — over an all-time window, the same funnel the hosted employer jobs table shows. jobStats.timeseries returns daily EmployerJobStatsPoint buckets (date, views, applyClicks) aggregated across the company's jobs to drive a chart; the optional since/until window defaults to the last 30 days. Both require an approved membership and are never cached. Analytics degrade to zeroes (or an empty series) rather than failing when the upstream analytics source is unavailable, so a 0 can mean either "no activity" or "analytics briefly unavailable."
profileStats.retrieve returns the company-level profileViews total — views of the company profile page itself (/companies/{slug}), with its tab subpaths (/jobs, /salaries) and bots excluded — over an all-time window, for a dashboard "profile views" metric. profileStats.timeseries returns daily EmployerProfileViewsPoint buckets (date, views) for a sparkline; the optional since/until window defaults to the last 30 days. Both require an approved membership, are never cached, and degrade to 0 / an empty series on an analytics outage.
Applicants and pipeline stages
The applicant read returns the job header, stage rail, applicants, signed resume links, and activity timelines in one EmployerPipeline. Pipeline mutations resolve void; re-read applicants.list for fresh state.
| Method and signature | Returns |
|---|---|
board.me.companies.applicants.list(slug: string, query: EmployerPipelineQuery, options?: FetchOptions) | Promise<EmployerPipeline> |
board.me.companies.applicants.move(slug: string, applicationId: string, body: MoveApplicantStageBody, options?: FetchOptions) | Promise<void> |
board.me.companies.applicants.bulkMove(slug: string, body: BulkMoveApplicantsBody, options?: FetchOptions) | Promise<void> |
board.me.companies.applicants.bulkReject(slug: string, body: BulkRejectApplicantsBody, options?: FetchOptions) | Promise<void> |
board.me.companies.applicants.addNote(slug: string, applicationId: string, body: AddApplicantNoteBody, options?: FetchOptions) | Promise<void> |
board.me.companies.pipelineStages.create(slug: string, body: CreatePipelineStageBody, options?: FetchOptions) | Promise<void> |
board.me.companies.pipelineStages.update(slug: string, stageId: string, body: UpdatePipelineStageBody, options?: FetchOptions) | Promise<void> |
board.me.companies.pipelineStages.remove(slug: string, stageId: string, options?: FetchOptions) | Promise<void> |
board.me.companies.pipelineStages.reorder(slug: string, body: ReorderPipelineStagesBody, options?: FetchOptions) | Promise<void> |
applicants.list requires { job: jobId } and accepts an optional stage filter. Bulk moves are atomic for applicants of one job. Company-private notes appear in the applicant timeline.
12345678910111213141516const pipeline = await board.me.companies.applicants.list('analytical-engines',{ job: jobId },privateOptions,);const firstApplicant = pipeline.applicants[0];if (firstApplicant) {await board.me.companies.applicants.move('analytical-engines',firstApplicant.id,{ stageId: interviewStageId },privateOptions,);}
Custom stages are limited to ten per job. Protected system stages cannot be removed, and a reorder must include each stage exactly once with the protected terminal order preserved.
Recommended talent
Candidates who fit a job this company posted and who have not applied to it, best match first. Ranking is computed server-side from the job (skills, title) against the candidate corpus and improves over time without any contract change.
| Method and signature | Returns |
|---|---|
board.me.companies.recommendedTalent.list(slug: string, query: RecommendedTalentListQuery, options?: FetchOptions) | Promise<ListEnvelope<RecommendedTalent>> |
query.job is required and must name a job belonging to :slug; a job that is still a draft raises job_not_published rather than returning an empty list, because only a published job carries the vectors a match needs. Requires an approved membership. Never cached.
This is a sourcing surface, and that shapes the contract. Applicants are excluded — they belong to the pipeline, and ranking them would be evaluation rather than discovery. Order is the ranking: no rank, score, band, or ranker identity appears in the response, now or later. The item wrapper carries only candidate today so that typed reasons and a coarse strength band can be added as minor releases. Do not build an automated reject, advance, or filter on this output.
Each item embeds the same talent_directory_entry the public directory returns, so directory rendering works unchanged. jobSearchStatus is populated here even for candidates who scoped it to employers only — the caller is a verified employer, which is what that setting is for. Only candidates who chose to be discoverable appear, and one whose status is not_looking never does. The list is a bounded top slice rather than everyone who matched, so expect it to be short — and note that low relevance alone does not empty it: v0 still returns its top slice on a weak board. An empty data means no candidate on that page survived — either nothing was ranked at all, or the entries for that page were dropped after ranking (profile deleted or hidden, or the candidate opted out since the last sync). It is not a relevance signal, and it can arrive mid-pagination, so follow hasMore / nextCursor rather than treating the first empty page as the end. Render it as "no candidates yet" rather than as an error.
12345678910const { data } = await board.me.companies.recommendedTalent.list('analytical-engines',{ job: jobId, limit: 20 },privateOptions,);for (const item of data) {// Order is the ranking — there is no score to sort or display.console.log(item.candidate.displayName, item.candidate.headline);}
Signed-in job alerts
Authenticated alerts are active immediately and do not use the anonymous double-opt-in flow. A user can have up to ten; removing an alert is the only pause mechanism.
| Method and signature | Returns |
|---|---|
board.me.alerts.list(options?: FetchOptions) | Promise<ListEnvelope<Alert>> |
board.me.alerts.create(body: AlertBody, options?: FetchOptions) | Promise<Alert> |
board.me.alerts.retrieve(alertId: string, options?: FetchOptions) | Promise<Alert> |
board.me.alerts.update(alertId: string, body: AlertBody, options?: FetchOptions) | Promise<Alert> |
board.me.alerts.remove(alertId: string, options?: FetchOptions) | Promise<void> |
update replaces the alert’s filters and frequency in full. Check board.context().features.jobAlerts before exposing the route, while still handling job_alerts_disabled from the API.
Recommended jobs
Personalized jobs for the signed-in candidate, best match first. Ranking is computed server-side from the candidate's profile (skills, roles, seniority, work authorization) and improves over time without any contract change — the response never exposes scores or ranker internals.
| Method and signature | Returns |
|---|---|
board.me.recommendedJobs.list(query?: RecommendedJobsListQuery, options?: FetchOptions) | Promise<ListEnvelope<RecommendedJob>> |
Each item wraps the same slim job_card the jobs list returns. Jobs the candidate already applied to are excluded; saved jobs are not. An empty list means the profile has no usable matching signal yet — read board.me.profile (parseStatus, skills) to decide whether to prompt for a resume upload rather than showing an empty rail.
1234567891011121314const { data } = await board.me.recommendedJobs.list({ limit: 20 },privateOptions,);if (data.length === 0) {const profile = await board.me.profile.retrieve(privateOptions);// Prompt for a resume when there is nothing to match on yet.console.log(profile.parseStatus);}for (const item of data) {console.log(item.job.title);}
Applications, resume, and saved jobs
Applications are keyed by application ID. Applying to a job itself lives at board.jobs.apply; this namespace manages applications already submitted by the current candidate.
| Method and signature | Returns |
|---|---|
board.me.applications.list(query?: ApplicationsListQuery, options?: FetchOptions) | Promise<ListEnvelope<Application>> |
board.me.applications.retrieve(applicationId: string, options?: FetchOptions) | Promise<Application> |
board.me.applications.updateFacts(applicationId: string, body: UpdateApplicationFactsBody, options?: FetchOptions) | Promise<Application> |
board.me.applications.withdraw(applicationId: string, options?: FetchOptions) | Promise<void> |
board.me.resume.upload(file: Blob, opts?: ResumeUploadOptions, options?: FetchOptions) | Promise<Resume> |
board.me.resume.retrieve(options?: FetchOptions) | Promise<Resume> |
board.me.resume.delete(options?: FetchOptions) | Promise<void> |
board.me.savedJobs.list(query?: SavedJobsListQuery, options?: FetchOptions) | Promise<ListEnvelope<SavedJob>> |
board.me.savedJobs.save(body: SaveJobBody, query?: Record<string, never>, options?: FetchOptions) | Promise<SavedJob> |
board.me.savedJobs.unsave(jobId: string, query?: Record<string, never>, options?: FetchOptions) | Promise<void> |
updateFacts is a merge-patch and is accepted only while the application is editable. withdraw permanently removes the application.
Resume upload creates multipart data internally and starts asynchronous parsing. opts accepts keepResumeOnFile, importMode: 'append_only' | 'replace_all', and confirmReplaceAll. Poll resume.retrieve until parseStatus is parsed or failed, then re-read the profile collections. Deleting the stored resume does not remove fields already imported into the profile.
1234567891011121314151617181920212223242526const uploaded = await board.me.resume.upload(file,{ keepResumeOnFile: true, importMode: 'append_only' },privateOptions,);async function waitForResume() {for (let attempt = 0; attempt < 30; attempt += 1) {const resume = await board.me.resume.retrieve(privateOptions);if (resume.parseStatus !== 'parsing') return resume;await new Promise((resolve) => setTimeout(resolve, 2_000));}throw new Error('Resume parsing did not finish within one minute');}const parsed =uploaded.parseStatus === 'parsing' ? await waitForResume() : uploaded;await board.me.savedJobs.save({ jobId },undefined,privateOptions,);console.log(parsed.parseStatus);
Saving an already-saved job converges on the existing row. Unsaving an unknown job ID still resolves 204.
Conversations and messages
Messaging is polled REST, not a realtime transport. Poll the conversation list, unread count, or current thread at a bounded interval—the reference cadence is 3–5 seconds—and cancel polling when the view is inactive.
| Method and signature | Returns |
|---|---|
board.me.conversations.list(query?: ConversationsListQuery, options?: FetchOptions) | Promise<ListEnvelope<Conversation>> |
board.me.conversations.unreadCount(options?: FetchOptions) | Promise<UnreadCount> |
board.me.conversations.retrieve(id: string, options?: FetchOptions) | Promise<ConversationDetail> |
board.me.conversations.listMessages(id: string, query?: ThreadMessagesQuery, options?: FetchOptions) | Promise<ListEnvelope<Message>> |
board.me.conversations.start(body: StartConversationBody, options?: FetchOptions) | Promise<Message> |
board.me.conversations.startAboutApplication(body: StartAboutApplicationBody, options?: FetchOptions) | Promise<Message> |
board.me.conversations.reply(id: string, body: ReplyBody, options?: FetchOptions) | Promise<Message> |
board.me.conversations.markRead(id: string, options?: FetchOptions) | Promise<ReadReceipt> |
board.me.conversations.archive(id: string, options?: FetchOptions) | Promise<ConversationArchive> |
board.me.conversations.unarchive(id: string, options?: FetchOptions) | Promise<ConversationArchive> |
board.me.conversations.findExisting(query: FindExistingConversationQuery, options?: FetchOptions) | Promise<ConversationRef> |
board.me.messages.edit(id: string, body: EditMessageBody, options?: FetchOptions) | Promise<Message> |
board.me.messages.unsend(id: string, options?: FetchOptions) | Promise<Message> |
board.me.messages.report(id: string, body: ReportBody, options?: FetchOptions) | Promise<ModerationReport> |
list({ archived: true }) returns the archived view. Thread messages are oldest-first; an unsent message remains as a tombstone with an empty body and deletedAt set.
start is the employer-to-candidate cold-message flow and converges on an existing thread. It accepts the recipient as exactly one of candidateBoardUserId or candidateHandle (the public handle from the talent surfaces — resolved server-side through the same gates). startAboutApplication contacts an applicant in application context. Message edit and unsend are limited to the author’s 15-minute window. Reporting a message addressed to the current user also blocks its author.
12345678910111213const { data: messages } = await board.me.conversations.listMessages(conversationId,{ limit: 50 },privateOptions,);const sent = await board.me.conversations.reply(conversationId,{ body: 'Thanks—let’s arrange a call.' },privateOptions,);console.log(messages.length, sent.id);
Blocks
| Method and signature | Returns |
|---|---|
board.me.blocks.list(options?: FetchOptions) | Promise<ListEnvelope<BlockedUser>> |
board.me.blocks.create(body: BlockUserBody, options?: FetchOptions) | Promise<Block> |
board.me.blocks.remove(boardUserId: string, options?: FetchOptions) | Promise<void> |
board.me.blocks.status(boardUserId: string, options?: FetchOptions) | Promise<BlockStatus> |
Creating and removing a block are idempotent. Blocking is silent; it changes the messaging relationship but does not notify the blocked user.
Candidate job access
Enumerate public offers with board.paywall.offers, then use the authenticated board.me.access methods to purchase and confirm an entitlement. Expose this flow only when board.context().features.candidatePaywall is true. Starting checkout also requires a candidate profile.
| Method and signature | Returns |
|---|---|
board.me.access.checkout(body: AccessCheckoutBody, options?: FetchOptions) | Promise<AccessCheckoutSession> |
board.me.access.retrieveCheckout(sessionId: string, options?: FetchOptions) | Promise<AccessCheckoutSessionState> |
board.me.access.grant(options?: FetchOptions) | Promise<AccessGrant> |
board.me.access.portal(body?: AccessPortalBody, options?: FetchOptions) | Promise<AccessPortalSession> |
checkout returns the connected-account mount values needed by Stripe.js. It does not load Stripe.js or mount the checkout UI. returnPath is a safe relative path.
Poll retrieveCheckout until the state is complete or expired; an open session can be remounted with its client secret. After completion, call grant again. No entitlement is represented by a successful { hasAccess: false } response, not a 404.
portal is available for a recurring grant and returns the billing-portal URL. Its optional body accepts a return path.
Pagination, features, and errors
List methods return ListEnvelope<T> and accept their documented cursor/limit query where present. Keep all board.me reads and writes out of shared public caches, including lists that appear non-sensitive.
Before rendering optional account routes, check the matching board context feature—especially candidates, employers, jobAlerts, and candidatePaywall. Feature flags improve navigation; the API remains authoritative and can reject stale or unauthorized state.
Every non-2xx API response throws BoardApiError. Common branches include:
- 401 for a missing or expired board-user token—enter the application’s explicit refresh/sign-in flow.
- 403 for another user’s resource, an unapproved employer membership, messaging restrictions, or entitlement restrictions.
- 404 for an ID that does not exist or does not belong to the current user.
- 409 for state conflicts such as a handle already taken.
- 429 for rate-limited messaging or other protected writes.
Domain codes give the precise branch: examples include candidate_handle_taken, applications_not_found, messaging_cold_rule, messaging_edit_window_expired, employer_not_member, employer_payment_required, employer_jobs_quota_exceeded, no_password, email_taken, last_admin, already_invited, invite_email_mismatch, company_deletion_disabled, paywall_no_candidate_profile, and paywall_offer_not_found.
Do not turn an exception into an empty list. Log code and requestId without logging authorization, resume URLs, checkout secrets, or candidate data, then preserve unexpected errors for the framework’s error boundary.