Embedded component

@payrollkit/react embeds PayrollKit's employer setup and payroll workflows in your React application with your configured branding, so employers can run payroll without leaving your product.

See Onboard an employer and Run payroll each period for the complete integration journeys.

Install

@payrollkit/react is the intended package name. The package has not been published; the Developer Preview will provide a tested prerelease version and installation instructions.

npm install @payrollkit/react

Create a session

Before showing PayrollKit, confirm that the signed-in user can access payroll for the Employer, then create an embedded session on your server.

  • Create a new session for each browser tab or full page load. If one page mounts more than one PayrollKit component at the same time, create a separate session for each component.
  • Pass its sessionToken directly to that page and keep it only in memory. The token is a secret, so never put it in a URL, browser storage, analytics or logs.
  • Store the session id on your server so you can revoke it when the user signs out or loses access.

Revoke sessions as follows:

  • On sign-out, revoke the current page's session.
  • When a user loses access to an Employer, revoke all their active sessions for that Employer.
  • When a user is disabled, revoke all their active sessions across every Employer.

Mount PayrollKit

import {
  PayrollKit,
  PayrollKitError,
  type PayrollKitHandle,
} from "@payrollkit/react";

const payrollKit = useRef<PayrollKitHandle>(null);

<PayrollKit
  ref={payrollKit}
  sessionToken={session.sessionToken}
  initialView={{ view: "PAY_RUN", payRunId: "payrun_a3f9c217" }}
/>

Component properties

export interface PayrollKitProps {
  sessionToken: string;
  initialView?: PayrollKitView;
  theme?: PayrollKitTheme;
  onReady?: () => void;
  onViewChanged?: (event: { view: PayrollKitView }) => void;
  onActionCompleted?: (event: PayrollKitActionCompletedEvent) => void;
  onSessionExpiring?: (event: PayrollKitSessionExpiringEvent) => void;
  onSessionEnded?: (event: PayrollKitSessionEndedEvent) => void;
  onError?: (error: PayrollKitError) => void;
}

export interface PayrollKitTheme {
  appearance?: "light" | "dark" | "system";
  accentColor?: `#${string}`;
}

export type PayrollKitAction =
  | "EMPLOYER_UPDATED"
  | "PAYE_SCHEME_UPDATED"
  | "EMPLOYMENT_UPDATED"
  | "PAY_RUN_UPDATED"
  | "PAY_RUN_APPROVED"
  | "BANK_PAYMENT_FILE_CREATED"
  | "ACCOUNTING_EXPORT_CREATED"
  | "PENSION_EXPORT_CREATED"
  | (string & Record<never, never>);

export interface PayrollKitActionCompletedEvent {
  eventId: string;
  action: PayrollKitAction;
  resource: {
    employerId?: string;
    employmentId?: string;
    payRunId?: string;
    bankPaymentFileId?: string;
    accountingExportId?: string;
    pensionExportId?: string;
    [resourceId: `${string}Id`]: string | undefined;
  };
}

export interface PayrollKitSessionEndedEvent {
  reason: "expired" | "revoked";
}

export interface PayrollKitSessionExpiringEvent {
  expiresAt: string;
}

export type PayrollKitErrorCode =
  | "component_load_failed"
  | "component_not_ready"
  | "component_unmounted"
  | "invalid_view"
  | "navigation_timed_out"
  | "network_unavailable"
  | "record_not_available"
  | "session_already_mounted"
  | "session_ended"
  | "session_invalid"
  | "session_replacement_failed"
  | "session_token_changed"
  | "unexpected_error"
  | "unsupported_component_version"
  | (string & Record<never, never>);

export declare class PayrollKitError extends Error {
  readonly code: PayrollKitErrorCode;
  readonly requestId?: string;
}

export interface PayrollKitHandle {
  navigate(view: PayrollKitView): Promise<void>;
  replaceSession(sessionToken: string): Promise<void>;
}
PropertyRequiredWhat it does
sessionTokenYesSupplies the session when the component is mounted. Use replaceSession() before expiry; mount separately when the Employer or user changes.
initialViewNoChooses the first view to display when the component mounts. PayrollKit opens HOME when omitted.
themeNoAdjusts the component's appearance. You can update it while the component is mounted.
onReadyNoTriggers once each time the component is mounted, after the session and first view have loaded.
onViewChangedNoTriggers when the current supported view changes.
onActionCompletedNoTriggers when a completed Employer action changes a resource.
onSessionExpiringNoTriggers once when the current token has five minutes remaining.
onSessionEndedNoTriggers when the session has expired or been revoked.
onErrorNoTriggers on an asynchronous component error that is not returned by a method call. PayrollKit's own error state does not depend on this callback.

Component lifecycle

When the component first mounts

  • Wait for onReady before calling navigate() or replaceSession(). Calls made earlier reject with component_not_ready.
  • If initialView is invalid or unavailable, PayrollKit reports the problem through onError, falls back to HOME and then calls onReady.

While the component is mounted

  • To change views, call navigate(). Changing initialView after mounting has no effect.
  • To renew a session for the same user and Employer, call replaceSession(). Changing sessionToken clears payroll information and calls onError with session_token_changed.
  • You can update theme and callback properties without remounting.

When the component is unmounted

  • PayrollKit stops all callbacks and discards unsaved input.
  • The session remains valid, and actions PayrollKit has already saved are not undone.
  • If you remount within the same page, you may reuse the valid in-memory token. Saved data reloads, but unsaved input is lost.

Views and navigation

PayrollKit sits inside your application, which continues to control the page URL, breadcrumbs and surrounding navigation.

  • Set initialView to choose which view PayrollKit shows when the component first loads. If you leave it out, PayrollKit opens HOME.
  • Call navigate() to change the view after the component has loaded.

Available views

export type PayrollKitView =
  | { view: "HOME" }
  | { view: "EMPLOYEES" }
  | { view: "EMPLOYEE"; employeeId: string }
  | { view: "PAY_RUNS" }
  | { view: "PAY_RUN"; payRunId: string };
ViewWhat it showsRequired ID
HOMESetup progress, recent Pay Runs and the next payroll actionNone
EMPLOYEESPayroll employees and setup issuesNone
EMPLOYEEOne employee's payroll detailsemployeeId
PAY_RUNSUpcoming and completed Pay RunsNone
PAY_RUNOne Pay Run, from resolving issues through review, approval and outputspayRunId

PayrollKit handles setup, issues, review and outputs within these views. You can link to a Pay Run without needing to track which step the employer is on.

Change the view

Call navigate() with one of the views above:

try {
  await payrollKit.current?.navigate({
    view: "PAY_RUN",
    payRunId: "payrun_a3f9c217",
  });
} catch (error) {
  if (error instanceof PayrollKitError) {
    handlePayrollNavigationError(error);
  }
}

navigate() only opens records belonging to the Employer associated with the session. Otherwise, it rejects with record_not_available.

onViewChanged runs when the Employer or navigate() moves PayrollKit to a different public view. It does not run for the initial view. Calling navigate() with the current view and resource ID resolves without emitting another event.

Events

Use component events to refresh your page while PayrollKit is mounted and respond when the session ends:

  • Use onActionCompleted as a prompt to fetch the affected resource after PayrollKit accepts and records an employer action.
  • Use signed webhooks for server-side notifications that can be recorded independently of the page, including later work such as HMRC filing and Payslip progress.
  • Use onSessionExpiring to replace a token before a long-running session ends.
  • When onSessionEnded fires, remove payroll information from the page. Request a new session if it expired. If it was revoked, confirm that the user still has access first.

Add only the callbacks your page needs:

<PayrollKit
  sessionToken={session.sessionToken}
  onViewChanged={({ view }) => updatePartnerRoute(view)}
  onActionCompleted={(event) => refreshPayrollResource(event)}
  onSessionExpiring={() => {
    void replaceEmbeddedSession().catch(handleSessionReplacementError);
  }}
  onSessionEnded={({ reason }) => handleSessionEnd(reason)}
  onError={(error) => showPayrollError(error)}
/>

Actions reported by PayrollKit

onActionCompleted tells your application what changed and includes the IDs needed to fetch the affected resource.

When this happensaction valueIDs returned
Employer updates their payroll detailsEMPLOYER_UPDATEDemployerId
Employer updates their PAYE Scheme detailsPAYE_SCHEME_UPDATEDemployerId
Employer updates an employee's payroll setupEMPLOYMENT_UPDATEDemploymentId
Employer resolves a Pay Run issue or creates a Pay Run Input RevisionPAY_RUN_UPDATEDpayRunId
Employer approves the reviewed calculationPAY_RUN_APPROVEDpayRunId
A requested accounting export record is createdACCOUNTING_EXPORT_CREATEDpayRunId, accountingExportId
A requested pension export record is createdPENSION_EXPORT_CREATEDpayRunId, pensionExportId
A requested bank payment file record is createdBANK_PAYMENT_FILE_CREATEDpayRunId, bankPaymentFileId

Accounting, pension and bank payment files are created only when requested. A CREATED event means that the record exists, but the file may still be processing. Fetch the record for its current status.

Callbacks may arrive late, more than once or out of order. They may also be lost when the page closes or the component unmounts. Earlier events are not replayed when PayrollKit mounts again.

Each completed action has a unique eventId. Repeated callbacks for that action use the same ID, so you can identify duplicates. The ID does not indicate order and is not shared with webhook deliveries. Match callbacks and webhooks using the affected resource IDs instead.

PayrollKit may add action values in backwards-compatible releases. Do not treat an unrecognised value as an error. Refresh the resource when your application supports the supplied ID; otherwise ignore that callback and use webhooks or an API read to reconcile the state you need.

Removing or renaming an action, changing its meaning or changing its required resource IDs is a breaking change. Resource ID properties in the TypeScript interface are optional so future actions can supply different IDs; check that an ID is present before using it.

Keep a long-running session active

Sessions expire after 60 minutes. Five minutes before expiry, onSessionExpiring runs so you can replace the session without interrupting the user.

Create the replacement on your server with a new Idempotency-Key, then pass its token to replaceSession():

const replaceEmbeddedSession = async () => {
  const replacement = await createEmbeddedSession();

  if (!payrollKit.current) {
    await revokeEmbeddedSession(replacement.id);
    return;
  }

  try {
    await payrollKit.current.replaceSession(replacement.sessionToken);
  } catch (error) {
    await handleSessionReplacementError(error, replacement);
  }
};

Retrying with the same replacement token succeeds if it is already active.

On success, PayrollKit preserves the current view and unsaved input, switches to the replacement and revokes the earlier session. Record the replacement id as the page's current session on your server and mark the earlier session as revoked.

If replacement fails:

  • For network_unavailable, retry replaceSession() with the same token. Do not create another session.
  • For session_replacement_failed, revoke the unused replacement. The earlier session remains active until it expires.
  • If no replacement succeeds before expiry, PayrollKit clears its content and calls onSessionEnded with expired.

expiresAt is the definitive expiry time. A suspended tab may delay onSessionExpiring, so you may replace the session earlier using that value.

Errors and recovery

PayrollKit separates method errors, asynchronous component errors and session endings:

  • Catch errors from navigate() and replaceSession() where you call them.
  • Use onError for component errors that happen outside a method call.
  • Use onSessionEnded when a session expires or is revoked.

A rejected method does not also call onError, and an expired or revoked session does not call onError. If a session ends while a method is pending, the method rejects with session_ended so the call does not remain pending; onSessionEnded still reports whether the session expired or was revoked.

Problems with Employer input or payroll actions are shown and resolved inside PayrollKit rather than reported as PayrollKitError.

Use the error code to decide what to do. Its message is safe to display, but the wording may change and must not control application behaviour. Include the optional requestId when contacting support.

PayrollKit may add error codes in a backwards-compatible release. Handle the codes you recognise and provide a generic fallback for any others. For an unrecognised rejected method error, keep the current Partner route and offer a retry. For an unrecognised onError value, leave the component visible and record its requestId when present. Removing a code or changing its meaning is a breaking change.

onError is optional. PayrollKit applies the documented recovery whether or not you provide it. If the component cannot continue, it removes payroll data and displays a safe error state before calling onError. That state remains if the callback is omitted or throws. Use onError only when your surrounding page also needs to respond or record the problem.

Errors reported through onError

CodeBehaviour and recovery
component_load_failedPayrollKit does not become ready or display payroll data. Remount once with the same valid token. If it fails again, check the registered origin and frame-src policy before contacting support.
session_invalidPayrollKit does not become ready or display payroll data. Create a new session. If that also fails, check that the page uses the expected environment and registered origin before contacting support.
session_already_mountedThis component does not become ready, but the existing component remains active. Unmount the other component before reusing its token, or create a separate session.
session_token_changedPayroll information is cleared and this component cannot become ready again. Remount with the correct session. For future renewals for the same user and Employer, use replaceSession().
unsupported_component_versionPayrollKit does not become ready or display payroll data. Upgrade @payrollkit/react or contact PayrollKit support.

View errors

During startup, these errors call onError, open HOME and then call onReady. After startup, navigate() rejects and the current view remains unchanged.

CodeBehaviour and recovery
invalid_viewCorrect the view or required ID before trying again.
record_not_availableCorrect the resource ID or return the user to HOME.

Rejected method calls

CodeBehaviour and recovery
component_not_readyThe component continues loading and the method is not run. Wait for onReady before trying again.
component_unmountedThe component is unmounted and sends no more callbacks. Stop handling the call; mount another component only if the page still needs payroll.
navigation_timed_outThe current PayrollKit view remains unchanged. Keep the current route and offer a retry.
network_unavailableThe current view and unsaved input remain. Retry navigate(), or retry replaceSession() with the same replacement token because its result may be uncertain.
session_replacement_failedThe earlier session, current view and unsaved input remain. Revoke the unused replacement and retry only after checking its user, Employer and origin.

Unexpected errors

CodeBehaviour and recovery
unexpected_errorThe method rejects when the failure is tied to a method call; otherwise PayrollKit uses onError. Payroll information is cleared and the component cannot continue. Remount once with the same valid session, then contact support with the requestId if it happens again.

When the session ends

CodeBehaviour and recovery
session_endedA pending method rejects with this code while onSessionEnded reports the reason. Payroll information and unsaved input are cleared, so stop handling the method call and follow onSessionEnded.

Browser behaviour

PayrollKit handles the browser behaviour needed to fit securely and accessibly into your page.

  • You can use @payrollkit/react in a server-rendered app. It waits until the page is running in the user's browser before loading PayrollKit.
  • PayrollKit checks session validity automatically. If a session expires or is revoked, PayrollKit clears the component and calls onSessionEnded. No Partner polling is required.
  • PayrollKit adjusts its height automatically, manages keyboard focus during navigation and dialogs, and supports screen readers.

Appearance

Use the theme property to match PayrollKit to your application:

SettingOptionsDefaultWhat it changes
appearancelight, dark, systemlightThe component's colour mode.
accentColorSix-digit hex colour#171717Interactive elements and highlights.

Partner logos and product names are configured during setup. Where the out-of-the-box options do not meet your needs, we will work with you during your integration to understand what you need and confirm what we can support.