Error codes
Complete BoardApiError catalog with guards, recovery decisions, retries, and observability.
A
JEvery non-2xx response throws BoardApiError. The client preserves the API’s machine code, human message, structured details, request ID, HTTP status, and parsed raw body.
If the response is not a parseable Cavuno error envelope, the SDK synthesizes unknown_error and retains the parsed body when one exists.
Handle an error once
Catch an error where the interface can make a recovery decision. Do not turn every error into an empty list.
Guards
| Guard | Exact match | Recovery |
|---|---|---|
isBoardApiError | Structural BoardApiError brand, numeric status, and string code | Log structured fields and use a general failure boundary. |
isNotFound | HTTP 404 | Render not found or try the managed redirect resolver. |
isUnauthorized | HTTP 401 | Run the host-owned explicit refresh flow or sign out. The SDK does not refresh automatically. |
isBoardPasswordRequired | 401 + board_password_required | Show the board-password gate, not board-user login. |
isForbidden | HTTP 403 | Explain or remove the action for the current viewer. |
isConflict | HTTP 409 | Re-read authoritative state before asking the user to resolve the conflict. |
isRateLimited | HTTP 429 | Back off. Capture Retry-After in onResponse if the application needs the response header. |
isValidationError | 400 + validation_bad_request | Interpret known details for field errors and keep an unknown-details fallback. |
The guards are structural rather than instanceof-only so they remain correct when separate helper bundles contain their own class copy.
Complete known-code catalog
This is the complete exported BOARD_API_ERROR_CODES catalog in version 1.34.0. Codes can be added compatibly within API v1, so a default branch remains mandatory.
| Family | Codes | Recovery and retry |
|---|---|---|
| Cross-cutting | internal_error, validation_bad_request, validation_payload_too_large, rate_limited, rate_limit_unavailable, search_unavailable, too_many_filter_values, pagination_invalid_cursor, pagination_offset_too_large, auth_unauthenticated, auth_forbidden, plan_upgrade_required, unknown_error | Correct validation, payload, filter, cursor, window, auth, or plan state. Back off public reads for rate/search/service failures; do not blindly retry writes or unknown outcomes. |
| Board resolution and pages | boards_not_found, board_page_not_found | Check the board identifier or route. Do not retry unchanged input. |
| Board-user authentication | board_auth_account_not_found, board_auth_email_taken, board_auth_invalid_credentials, board_auth_invalid_token, board_auth_registration_disabled, board_auth_token_expired, board_development_origin_not_allowed_for_email, board_development_origin_not_registered, board_development_origin_requires_publishable_key, invalid_current_password, no_password, same_email, email_taken, invalid_token | Render the matching registration, sign-in, password, or email-change state. Credential failures are not transient. Passwordless accounts set a password with board.auth.forgotPassword; updatePassword returns no_password. email_taken on an email change is 400 and is distinct from registration’s 409 board_auth_email_taken. board_auth_account_not_found is 404 from requestMagicLink with intent: 'sign_in' when no account exists for that email. board_development_origin_not_registered is 400 when developmentOrigin names an origin the board has not registered; add it under Settings → SDK → Development origins. board_development_origin_requires_publishable_key is 400 when developmentOrigin is sent without a publishable-key (pk_...) board identifier. board_development_origin_not_allowed_for_email is 400 when register, forgotPassword, or requestMagicLink names a non-localhost development origin: email links can only target localhost, so test email sign-in there and use Google or LinkedIn sign-in on a preview origin. |
| Board-user sign-in methods and SSO | board_auth_method_unavailable, board_auth_sso_connection_not_found, board_auth_sso_role_unavailable, board_auth_sso_provider_unavailable, board_auth_sso_browser_mismatch, board_auth_sso_not_provisioned, board_auth_sso_identity_linked_elsewhere | board_auth_method_unavailable (403) means that sign-in method is switched off for the role: offer the built-in methods and SSO connections in details.availableMethods (isSignInMethodUnavailable) instead. Refresh board.context() after a connection or role error; retry board_auth_sso_provider_unavailable later. On board_auth_sso_browser_mismatch, ask the user to open the emailed link where they started signing in. |
| Board password | board_password_required, board_password_invalid | Request or re-request the board password grant. Do not treat this as a board-user 401. |
| Jobs | jobs_not_found, jobs_invalid_filter | Return 404 for a missing job; correct or remove an invalid filter. |
| Taxonomy | categories_not_found, skills_not_found, places_not_found | Resolve a canonical slug or return 404. |
| Locations | locations_invalid_id, locations_unavailable | Ask the user to pick the location again from board.locations.search, or retry later while the search is unavailable. |
| Companies | companies_not_found, company_markets_not_found, company_salary_not_found, company_category_salary_not_found | Return 404 or remove a link to data that no longer exists. |
| Salaries | salaries_title_not_found, salaries_skill_not_found, salaries_location_not_found, salaries_title_location_not_found, salaries_skill_location_not_found | Redirect a resolved alias or return 404. Do not create an empty indexable salary page. |
| Blog | blog_post_not_found, blog_author_not_found, blog_tag_not_found | Honor post redirects when returned; otherwise render 404. |
| Talent | talent_not_found, talent_directory_not_found, talent_directory_restricted | Distinguish a missing/private profile, disabled directory, and employer-only access. Restricted access can lead to employer sign-in or an upsell. |
| Job alerts | job_alert_not_found, job_alerts_disabled, candidate_alert_limit_reached, candidate_alert_not_found | Render missing/disabled state or ask the signed-in candidate to remove an alert before creating another. Retry only after state changes. |
| Candidate accounts | candidate_entry_not_found, candidate_handle_taken, candidate_job_not_found, candidate_profile_required, resume_invalid_file, resume_upload_forbidden | Reload a deleted entry, choose another handle, or correct file/permission state. candidate_profile_required means the signed-in account is not a candidate (for example an employer): show the employer profile view (avatar, displayName, link to employer screens) instead of candidate profile forms. Do not automatically repeat an upload. |
| Applications | applications_external_apply_only, applications_guest_not_allowed, applications_job_not_found, applications_native_disabled, applications_not_found, applications_resume_invalid_file, applications_unprocessable | Open the external apply URL, request sign-in, return 404, correct the file, or show the server’s workflow rejection. Native apply itself converges on an existing application; other writes are not blanket-retried. |
| Messaging | messaging_application_not_found, messaging_application_not_linked, messaging_blocked, messaging_cannot_block_self, messaging_cold_rule, messaging_conversation_not_found, messaging_disabled, messaging_edit_window_expired, messaging_message_deleted, messaging_message_not_found, messaging_not_author, messaging_not_participant, messaging_not_permitted, messaging_not_recipient, messaging_rate_limited, messaging_recipient_not_found, messaging_recipient_not_open, messaging_talent_access_required, messaging_unsend_window_expired | Refresh the thread and render the precise permission, lifecycle, or moderation outcome. Back off messaging_rate_limited; do not replay message writes automatically. |
| Employer self-service | employer_applicant_not_found, employer_ats_unprocessable, employer_checkout_failed, employer_company_exists, employer_company_name_taken, employer_company_not_found, employer_invite_not_found, employer_job_not_found, employer_job_slug_taken, employer_jobs_quota_exceeded, employer_member_not_found, employer_not_member, employer_payment_required, employer_pipeline_stage_not_found, not_company_admin, not_company_member, company_deletion_disabled, last_admin, already_member, already_invited, invalid_email, invite_email_mismatch, candidate_role, talent_list_not_found, sourced_candidate_not_found, sourced_convert_unavailable | Re-read the membership, job, applicant, pipeline, quota, billing, talent-list, sourced, and invite state. Ask the employer to resolve naming, membership, payment, role, or workflow issues before retrying. sourced_convert_unavailable is 409 when the board has no native pipeline — do not map it to employer_company_name_taken. |
| Candidate paywall | candidate_paywall_access_required, paywall_already_active, paywall_disabled, paywall_invalid_checkout_session, paywall_no_candidate_profile, paywall_no_recurring_subscription, paywall_offer_not_found | Re-read offers and the access grant, create the required profile, or hide recurring management for lifetime access. Never infer access from a retry or redirect. candidate_paywall_access_required is 403 from recommended jobs and job alerts when the board's paywall is active and the candidate's job seeker plan does not unlock that feature; render the paywall offers in place of the feature rather than pre-gating from context flags. |
| Memberships | membership_required | 403 on a posting attempt when the board requires an approved membership and the viewer's company holds none. Render a join gate that links to the board's membership plans (board.plans.list({ purpose: 'membership' })). Read board.context().posting.requiresMembership first so the gate replaces the form before anyone submits. |
| Employer talent-access paywall | talent_access_unavailable, company_required, already_on_plan, talent_access_required, messaging_talent_access_required, stripe_not_connected | Hide or disable talent checkout when the board does not sell it; ask the viewer to pick a company when they have more than one membership; use in-place upgrade rather than a second checkout when already subscribed; spend unlocks/messages only with remaining credits. messaging_talent_access_required is also listed under Messaging. |
| Public job posting | job_posting_logo_lookup_unavailable, job_posting_logo_not_found, job_posting_rejected | Continue without automatic logo discovery when appropriate, or show the posting rejection. Do not automatically resubmit a paid posting. |
Retry rules
The SDK performs exactly one fetch for every method call.
- Public reads can be retried by the host with bounded backoff after 429 or transient 5xx/search failures.
- A 401 refresh is explicit because refresh tokens rotate. Coordinate one refresh, persist the new pair, then retry the owning read once.
- Do not retry validation, permission, not-found, plan, quota, or conflict errors until input or server state changes.
- Do not blanket-retry writes. Some individual operations converge or support a specific retry contract, but the SDK provides no global mutation idempotency promise.
- For a network failure after a write, the result can be unknown. Re-read state before offering another submission.
Observability and user copy
Use code for control flow and translation lookup. The server’s message is useful for diagnostics but is not a stable localization key. Treat details as unknown until narrowed for the specific code.
Log code, status, requestId, route or operation name, and non-sensitive context. Never log access tokens, password grants, resumes, application content, messages, checkout secrets, or the entire raw body by default.
Verify error handling
Request a known missing resource in development. Confirm that the UI reaches the 404 branch and the diagnostic event contains the server request ID when one was returned. Then simulate an unknown code and confirm the default branch remains reachable.