@hiofu/apply-sdk
v0.1.7
Published
Browser SDK for HIOFU Apply with OAuth, HSP1 review, and idempotent application submission.
Readme
@hiofu/apply-sdk
Browser SDK for HIOFU Apply.
Use it to add a HIOFU-powered application flow to job boards, ATS products, and employer career sites. The SDK handles:
- OAuth 2.0 Authorization Code with PKCE
- HSP1 review and candidate consent in a popup
- role routing from your job/page IDs to HIOFU roles
- immutable application snapshot creation
- idempotent application submission
- structured success, cancellation, and error handling
Install
npm install @hiofu/apply-sdkQuick Start
React Button
import { HiofuApplyButton } from "@hiofu/apply-sdk/react";
export function ApplyButton({ jobId, jobTitle }) {
return (
<HiofuApplyButton
clientId={process.env.NEXT_PUBLIC_HIOFU_KEY}
variant="primary"
options={{
jobId,
jobTitle,
}}
/>
);
}The React button uses the lower-level HiofuClient API, where the publishable
key is passed as clientId.
TypeScript Client
import { createApplyClient } from "@hiofu/apply-sdk";
const hiofu = createApplyClient({
publicKey: "pk_test_xxx",
});
const result = await hiofu.apply({
jobId: "job_123",
externalEmployerId: "emp_456",
employerName: "Acme Inc",
role: {
title: "Senior Engineer",
description: "Build and operate HIOFU integrations.",
locations: ["London", "Remote"],
skills: ["TypeScript", "API integrations"],
},
idempotencyKey: "apply_attempt_01JXYZ",
});
console.log(result.applicationId);
console.log(result.raw.application.status);createApplyClient is the recommended imperative browser API for new
non-React integrations. It resolves HIOFU URLs from the key prefix and returns a
normalized result while keeping the full API response under result.raw.
Environments
Switch environments by changing the publishable key.
| Mode | Publishable key | HIOFU origin | API base |
| --- | --- | --- | --- |
| Sandbox | pk_test_* | https://sandbox.hiofu.com | https://api.sandbox.hiofu.com/api |
| Production | pk_live_* | https://hiofu.com | https://api.hiofu.com/api |
const hiofu = createApplyClient({
publicKey:
process.env.NODE_ENV === "production" ? "pk_live_xxx" : "pk_test_xxx",
});pk_test_* keys can only talk to sandbox endpoints. pk_live_* keys can only
talk to production endpoints.
createApplyClient
import { createApplyClient } from "@hiofu/apply-sdk";
const hiofu = createApplyClient({
publicKey: "pk_test_xxx",
storage: "memory",
popupOptions: {
timeoutMs: 300_000,
},
onComplete(result) {
console.log("Application submitted", result.applicationId);
},
onCancel(context) {
console.log("Apply cancelled", context.reason);
},
onError(error) {
console.warn(error.code, error.correlationId, error.retryable);
},
});Configuration:
publicKey(required): publishable key, for examplepk_test_xxxorpk_live_xxx.environment: inferred from the key prefix. Override with"sandbox"or"production"only when you need explicit validation.redirectUri: defaults to the HIOFU-hosted callback. Set this only when you host your own callback page.scopes: default scopes forauthorize().applyScopes: default scopes forapply().storage:"memory"by default, or"session"to persist the access token for the browser session.popupOptions.timeoutMs: popup wait limit in milliseconds. Defaults to300_000.onComplete(result): receives a normalized apply result.onCancel(context): receives user-cancel, popup-close, and timeout events.onError(error): receives a typedHiofuApplyError.onEvent(event): receives lower-level lifecycle events.
hiofuOrigin and apiBase are also available for local/internal testing. Most
partner integrations should rely on publicKey and environment instead.
Apply Payload
createApplyClient.apply() accepts partner-owned job and role data:
await hiofu.apply({
jobId: "job_123",
externalEmployerId: "emp_456",
employerName: "Acme Inc",
jobTitle: "Senior Engineer",
role: {
title: "Senior Engineer",
description: "Role description from your system.",
location: "London",
locations: ["London", "Remote"],
salary: "GBP 90k-110k",
skills: ["TypeScript", "Node.js"],
dimensions: {
backend: 5,
productJudgement: 4,
},
metadata: {
source: "ats",
},
},
partnerTags: {
campaign: "summer-hiring",
},
callbackState: "opaque_state_from_your_app",
idempotencyKey: "apply_attempt_01JXYZ",
});Rules:
- Provide either
jobIdorexternalJobId. role.titleis required and becomesjobTitlewhenjobTitleis omitted.externalEmployerIdis optional for single-employer integrations, but recommended for multi-employer job boards.idempotencyKeyis required forcreateApplyClient.apply(). Use a stable partner-generated application attempt ID.variationIdis optional. When omitted, the popup asks the candidate which HSP1 view to share.
Normalized Result
type HiofuNormalizedApplyResult = {
applicationId: string;
partnerApplicationId?: string;
hiofuRoleId: string;
status: string;
environment: "sandbox" | "production";
submittedAt: string;
raw: HiofuApplyResult;
};Use raw when you need snapshot, profile, evidence, delivery, or authorization
details returned by the HIOFU API.
React
React exports live at @hiofu/apply-sdk/react.
Standalone Button
import { HiofuApplyButton } from "@hiofu/apply-sdk/react";
<HiofuApplyButton
clientId="pk_test_xxx"
variant="primary"
options={{
jobId: "job_123",
jobTitle: "Senior Engineer",
employerId: "emp_456",
employerName: "Acme Inc",
}}
onSuccess={(result) => {
console.log(result.application.id);
}}
onError={(error) => {
console.error(error.message);
}}
/>HiofuApplyButton variants are primary, secondary, and ghost. The button
creates its own HiofuClient when clientId is provided.
Provider
Use a provider when multiple buttons or hooks share the same client config.
import { HiofuApplyButton, HiofuProvider } from "@hiofu/apply-sdk/react";
<HiofuProvider
config={{
clientId: "pk_test_xxx",
storage: "memory",
}}
>
<HiofuApplyButton
options={{ jobId: "job_1", jobTitle: "Role A" }}
/>
<HiofuApplyButton
options={{ jobId: "job_2", jobTitle: "Role B" }}
/>
</HiofuProvider>;Custom Button
import { HiofuProvider, useHiofuApply } from "@hiofu/apply-sdk/react";
function CustomApplyButton() {
const { apply, status, error, result, reset } = useHiofuApply();
const busy = status === "authorising" || status === "submitting";
return (
<div>
<button
disabled={busy}
onClick={async () => {
try {
await apply({
jobId: "job_123",
jobTitle: "Senior Engineer",
employerId: "emp_456",
employerName: "Acme Inc",
});
} catch {
// State is exposed through `error`.
}
}}
>
{status === "success" ? "Submitted" : "Apply with HIOFU"}
</button>
{status === "error" && <p role="alert">{error?.message}</p>}
{status === "success" && (
<p>Application ID: {result?.application.id}</p>
)}
{(status === "error" || status === "success") && (
<button onClick={reset}>Try again</button>
)}
</div>
);
}
export function Page() {
return (
<HiofuProvider config={{ clientId: "pk_test_xxx" }}>
<CustomApplyButton />
</HiofuProvider>
);
}useHiofuApply() must be used inside HiofuProvider.
Lower-Level HiofuClient
HiofuClient remains available for compatibility and for integrations that
need direct access to authorization/profile helpers.
import { HiofuClient } from "@hiofu/apply-sdk";
const hiofu = new HiofuClient({
clientId: "pk_test_xxx",
storage: "session",
authorizeTimeoutMs: 300_000,
});
const result = await hiofu.apply({
jobId: "job_123",
jobTitle: "Senior Engineer",
employerId: "emp_456",
employerName: "Acme Inc",
});
console.log(result.application.id);With HiofuClient.apply(), idempotencyKey is optional. If omitted, the SDK
generates one for the attempt and sends it in both the request header and body.
Hosted Script
Use the hosted/global build for static pages or no-build job boards.
<script
src="https://cdn.hiofu.com/apply-sdk.global.js"
data-client-id="pk_test_xxx"
></script>
<button
data-hiofu-apply
data-job-id="job_123"
data-job-title="Senior Engineer"
data-employer-id="emp_456"
data-employer-name="Acme Inc"
data-external-role-id="job_123"
data-external-employer-id="emp_456"
data-role-title="Senior Engineer"
data-role-department="Engineering"
data-role-locations="London,Remote"
>
Apply with HIOFU
</button>Required button attributes:
data-job-iddata-job-title
Optional button attributes include:
data-employer-iddata-employer-namedata-hiofu-scopesdata-hiofu-variation-iddata-hiofu-idempotency-keydata-external-role-iddata-external-employer-iddata-role-titledata-role-descriptiondata-role-departmentdata-role-locationsdata-role-job-typesdata-role-salary-mindata-role-salary-maxdata-role-salary-currencydata-role-experience-mindata-role-experience-max
Listen for DOM events to update your UI:
<script>
document.addEventListener("hiofu:apply:success", (event) => {
console.log(event.detail.application.id);
});
document.addEventListener("hiofu:apply:error", (event) => {
console.error(event.detail);
});
</script>Script attributes:
data-client-id: required publishable key.data-hiofu-origin: optional local/internal origin override.data-api-base: optional local/internal API override.data-redirect-uri: optional partner-hosted callback URI.data-scopes: optional authorization scopes.data-apply-scopes: optional apply scopes.
Role Routing
Keep sending the same job or page ID your system already uses. Before going live, save a mapping from that ID to the destination HIOFU role in Developer settings or with the management client:
your job/page ID -> HIOFU roleFor single-employer integrations, HIOFU can derive the workspace from the
publishable key. For multi-employer sites, pass externalEmployerId with
createApplyClient or employerId/role.externalEmployerId with
HiofuClient.
The browser SDK should send your own job/page ID, not an internal HIOFU role ID. If no active mapping exists for the current environment, the API rejects the submission and no application is created.
Management Client
The management client automates setup actions from trusted backend code or internal tooling. Do not expose its access token to browsers.
import { createManagementClient } from "@hiofu/apply-sdk";
const management = createManagementClient({
apiBase: "https://api.sandbox.hiofu.com/api",
accessToken: process.env.HIOFU_EMPLOYER_ACCESS_TOKEN,
});Create or update a destination role:
const role = await management.createRole({
title: "Senior Engineer",
department: "Engineering",
description: "Role description from your system.",
locations: ["London", "Remote"],
});
await management.updateRoleStatus(role.id, "hiring");Issue a publishable key and map your external role ID:
await management.issuePublishableKey();
await management.saveRoleMapping({
mode: "test",
externalRoleId: "job_123",
externalEmployerId: "emp_456",
employerRoleId: role.id,
});Register a callback URI only when you override the HIOFU-hosted callback:
await management.addRedirectUri({
mode: "test",
uri: "https://jobs.example.com/oauth/callback.html",
});Other available management methods:
listRoles(params)getRole(roleId)updateRole(roleId, input)updateRoleStatus(roleId, status)getDeveloperSettings()removeRedirectUri(uriId)
Custom Callback Handling
Most integrations do not need a callback page. When redirectUri is omitted,
the SDK uses the HIOFU-hosted callback for the key's environment.
Only host your own callback when you have a specific same-origin requirement.
In that case, register the exact callback URL in HIOFU Developer settings and
pass it as redirectUri.
<!doctype html>
<html lang="en">
<meta charset="UTF-8" />
<title>Returning</title>
<body>
Returning to the application
<script>
(() => {
const p = new URLSearchParams(window.location.search);
const payload = {
type: "hiofu_oauth",
code: p.get("code"),
state: p.get("state"),
error: p.get("error"),
variationId: p.get("variation_id"),
};
if (window.opener) {
try {
window.opener.postMessage(payload, window.location.origin);
} catch {}
}
try {
if (typeof BroadcastChannel !== "undefined") {
const ch = new BroadcastChannel("hiofu_oauth");
ch.postMessage(payload);
ch.close();
}
} catch {}
try {
localStorage.setItem("hiofu_oauth_result", JSON.stringify(payload));
} catch {}
setTimeout(() => {
try {
window.close();
} catch {}
}, 150);
})();
</script>
</body>
</html>Scopes
profile.basic: minimal profile summary for your apply UX.profile.full: sanitized HSP1 profile summary including skills and redacted work/education history.evidence.read: evidence titles, types, and aggregate trust signals.applications.write: submit an application on the candidate's behalf.passport.snapshot: create and return an immutable hiring snapshot with summary previews.
apply() requests the minimal browser-safe bundle by default:
applications.writepassport.snapshot
The SDK automatically includes those two scopes for application submissions.
Events
Subscribe with onEvent or HiofuClient.subscribe():
popup_openedpopup_result_receivedpopup_closedtoken_issuedapply_startedapply_submittingapply_successapply_error
Errors
createApplyClient wraps runtime failures in HiofuApplyError:
import { HiofuApplyError, createApplyClient } from "@hiofu/apply-sdk";
const hiofu = createApplyClient({
publicKey: "pk_test_xxx",
});
try {
await hiofu.apply({
jobId: "job_123",
role: { title: "Senior Engineer" },
idempotencyKey: "apply_attempt_01JXYZ",
});
} catch (error) {
if (error instanceof HiofuApplyError) {
console.warn(error.code, error.correlationId, error.retryable);
}
}Error codes:
configuration_errorenvironment_mismatchpopup_blockedpopup_closedauth_failedconsent_requiredrole_mapping_failedsubmit_failedtimeout
Lower-level exports:
HiofuConfigurationError: invalid SDK configuration.HiofuApiError: HTTP errors from the HIOFU API, includingstatusand parsedbodywhen available.HiofuPopupError: popup closed or timed out before authorization completed.
Security Notes
- Prefer the HIOFU-hosted callback.
- Register only callback domains you control.
- Browser integrations receive a short-lived access token through the SDK runtime. Refresh tokens are not exposed to your UI code.
- The SDK defaults to in-memory token storage.
- Session storage persists access tokens only and strips refresh tokens.
- Do not log raw application payloads in production UIs.
- Use a stable idempotency key when your system already has an application attempt ID.
- The popup times out after 5 minutes by default. Use
popupOptions.timeoutMswithcreateApplyClientorauthorizeTimeoutMswithHiofuClient.
Brand Helpers
The package exports HIOFU brand constants and an inline SVG mark:
import {
HIOFU_BRAND_COLOR,
HIOFU_BUTTON_LABEL,
HIOFU_WORDMARK,
HIOFU_TAGLINE,
hiofuLogoSvg,
} from "@hiofu/apply-sdk";The SDK does not inject third-party fonts or global styles into your app. Your app should own its font-loading strategy.
Examples
Working examples live in the repository's examples/ directory:
- static HTML
- Next.js job board
- vanilla TypeScript SPA
- Express webhook receiver
