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:

OperationFailure surface
Initial status fetch on mountRenders an error alert; onError fires
Submission POSTonError 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_secret exchange 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 key whenever the token changes. It's the safest reset — a new bearerToken alone 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 useEffect with empty deps for the initial mint and the onError handler for refresh — not an unguarded call in the render path.
  • Forgetting key on remount. A new bearerToken without a key change can keep stale state, including a cached unauthorized error.
  • Catching 401 only on submit. The mount-time status fetch fails first. Your onError handler has to cover the initial-load case, not just submission.
  • Treating the document URL as long-lived. useTaxbit auto-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