Handling token expiration
Detect and recover from Account Owner token expiration in the SDK.
The SDK doesn't refresh Account Owner tokens automatically — when a token expires during an active session, the SDK's API calls fail with 401 Unauthorized and surface through your onError callback. This page covers how to detect expiration and recover gracefully when it happens.
For the broader auth flow and how to mint tokens, see Connect your backend.
The token model
Your server exchanges your client_id + client_secret for a tenant-scoped token, then exchanges that for an Account Owner–scoped token. The Account Owner token is the one you pass to the SDK via bearerToken. The SDK uses it for every call it makes during the component's lifetime — submission POST, status fetch, prior-submission GET, document URL generation. If it expires mid-session, all of those fail until you supply a new one.
What happens when a token expires
The Taxbit API returns 401 Unauthorized, and the SDK propagates it through your callbacks:
| Operation | Failure surface |
|---|---|
| Initial status fetch on mount | Renders an error alert; onError fires |
| Submission POST | onError fires with the failed request |
Document URL generation (useTaxbit) | error field on the hook is set; onError fires if you passed one to it |
Status polling (useTaxbit) | Fetches stop returning fresh data; error is set |
The error passed to onError is a standard Error object. The SDK doesn't parse the status code into a structured shape today — inspect the message, or rely on the fact that an active-session error after mount is almost always a 401.
Recover after expiration
When onError fires with what looks like an expired token, mint a fresh token server-side and re-render the SDK with the new value. Change the key prop to force React to unmount and remount with fresh state — the safest way to swap credentials:
function TaxFormPage() {
const [bearerToken, setBearerToken] = useState(null);
const [tokenIssuedAt, setTokenIssuedAt] = useState(null);
const refresh = useCallback(async () => {
const token = await mintToken(); // server endpoint that returns a fresh AO token
setBearerToken(token);
setTokenIssuedAt(Date.now());
}, []);
// Mint on first render
useEffect(() => { refresh(); }, [refresh]);
const handleError = async (err) => {
if (err.message.includes('401') || isExpiredError(err)) {
await refresh();
return;
}
reportToSentry(err);
};
if (!bearerToken) return <Spinner />;
return (
<TaxbitQuestionnaire
key={tokenIssuedAt} // forces remount on token refresh
bearerToken={bearerToken}
questionnaire="W-FORM"
onError={handleError}
onSuccess={() => router.push('/done')}
/>
);
}Change key on every token swap. Without it, React reconciles the same instance with new props and can keep stale internal state — including the cached 401. Changing key forces a full remount, so the SDK re-runs its initial status fetch with the new token.
Best practices
- Mint server-side, never in the browser. The
client_secretexchange must happen on your backend. The browser only ever sees the Account Owner token — narrow by design (one user, short window). - Mint just-in-time. Don't pre-mint at session start and hold the token. Mint when the SDK is about to render, so the live window is roughly the user's actual interaction time.
- Handle 401s explicitly in
onError. Distinguish expected expiration (re-mint and continue) from genuine errors (report to monitoring). Both surface through the same callback. - Force a remount with
keywhenever the token changes. It's the safest reset — a newbearerTokenalone isn't always picked up cleanly mid-render. - Keep tokens in memory only. Don't persist to client storage. Scope the token to the component or page where the SDK lives; when the tab closes or the user navigates away, the token is gone.
- Reuse one token across SDK calls in a session. The SDK uses a single token for status, submission, and document operations. Minting per operation is wasteful and adds avoidable identity-service load.
Pitfalls
- Minting on every render. Use
useEffectwith empty deps for the initial mint and theonErrorhandler for refresh — not an unguarded call in the render path. - Forgetting
keyon remount. A newbearerTokenwithout akeychange can keep stale state, including a cached unauthorized error. - Catching 401 only on submit. The mount-time status fetch fails first. Your
onErrorhandler has to cover the initial-load case, not just submission. - Treating the document URL as long-lived.
useTaxbitauto-refreshes it while the bearer is valid; once the bearer expires, the refresh fails. Don't store the URL outside the SDK's render lifecycle. - Issuing tokens before step-up auth. Even a short-lived Account Owner token in the browser is a sensitive credential. If a sensitive area requires 2FA or recent auth, gate the mint endpoint on that — not just session presence.
Next steps
- Integration Guide — the full auth and submission flow
- Component & Hook Reference —
onError,bearerToken, and theuseTaxbitreturn values

