Reading curing statuses

Read OPEN, IN_REVIEW, and RESOLVED curing statuses from the hook, server-side, and webhooks.

Curing statuses live on each issue inside statusData.wFormQuestionnaire.issues. The useTaxbit hook exposes them through statusData, and the same data is available server-side via the status endpoint and the ACCOUNT_OWNER_TAX_DOCUMENTATION_STATUS webhook. This page covers how to read each status, how to keep the data fresh, and how to handle the lifecycle outside the browser.

For the embedding walkthrough, see the Curing Integration Guide. For the full hook signature and return shape, see the Component and Hook Reference.

Where the statuses live

The hook makes one call to GET /tax-documentation-status on mount and exposes the response as statusData. Curing statuses are per-issue:

statusData?.wFormQuestionnaire?.issues
// Array of:
// {
//   issueType: 'US_INDICIA' | 'TREATY_COUNTRY_MISMATCH' | …,
//   status:    'OPEN' | 'IN_REVIEW' | 'RESOLVED',
//   createdAt: '2026-06-15T17:42:11Z',
//   details:   [{ field, description }, …],
// }

A convenience boolean, needsCuringDocumentation, is also exposed on the hook — it's the roll-up of "is there at least one OPEN curable issue?". For full lifecycle visibility (in-review state, resolved history), read issues[] directly.

What the hook returns

const {
  statusData,                   // the full status object
  needsCuringDocumentation,     // boolean — OPEN curable issue exists
  isLoading,                    // true while the status fetch is in flight
  refresh,                      // re-fetch status + submission, returns Promise<void>
  refreshStatus,                // re-fetch status only
  error,                        // fetch error, if any
} = useTaxbit({ bearerToken, questionnaire: 'W-FORM' });

Curable issue types are the four W-8 issues the SDK's curing flow can resolve. Define this set once and reuse it for filtering:

const CURABLE_ISSUE_TYPES = new Set([
  'US_INDICIA',
  'TREATY_COUNTRY_MISMATCH',
  'CARE_OF_PERMANENT_ADDRESS',
  'PO_BOX_PERMANENT_ADDRESS',
]);

Reading each status

Open issues — the filer needs to act

const openCurableIssues = (statusData?.wFormQuestionnaire?.issues ?? [])
  .filter((issue) => CURABLE_ISSUE_TYPES.has(issue.issueType))
  .filter((issue) => issue.status === 'OPEN');

Use this to drive a "your tax documentation needs attention" badge in your account UI or to gate the <TaxbitCuringDocumentation /> mount. Equivalent to checking needsCuringDocumentation, but exposes the actual issue list so you can show the filer what's outstanding before they land on the curing page.

In-review issues — the filer has submitted, your reviewer needs to act

const inReviewIssues = (statusData?.wFormQuestionnaire?.issues ?? [])
  .filter((issue) => CURABLE_ISSUE_TYPES.has(issue.issueType))
  .filter((issue) => issue.status === 'IN_REVIEW');

Use this to show "You've submitted your documentation — we're reviewing it" in your filer-facing UI. The <TaxbitCuringDocumentation /> widget renders nothing while a filer's curable issues are all IN_REVIEW, so you'll want your own confirmation surface during this window.

Resolved issues — closed, audit history

const resolvedIssues = (statusData?.wFormQuestionnaire?.issues ?? [])
  .filter((issue) => CURABLE_ISSUE_TYPES.has(issue.issueType))
  .filter((issue) => issue.status === 'RESOLVED');

Useful for compliance audit views or for showing a filer their history. The status will not change again from RESOLVED.

All statuses in one pass

const issuesByStatus = (statusData?.wFormQuestionnaire?.issues ?? []).reduce(
  (acc, issue) => {
    if (!CURABLE_ISSUE_TYPES.has(issue.issueType)) return acc;
    (acc[issue.status] ??= []).push(issue);
    return acc;
  },
  {}  // { OPEN?: [...], IN_REVIEW?: [...], RESOLVED?: [...] }
);

Single reduce, all three buckets. Drop straight into a filer dashboard summary.

Refreshing after the filer submits

The hook reads the status endpoint once on mount. After a successful cure submission, the server-side status has changed but statusData won't reflect it until you re-fetch. Wrap the SDK's onSuccess callback:

<TaxbitCuringDocumentation
  bearerToken={bearerToken}
  onSuccess={async () => {
    await refresh();
    router.push('/account');
  }}
/>

refresh() returns a Promise<void> that resolves when both the status and submission fetches settle, so you can await it before reading derived state. Use refreshStatus() if you only need the status object updated.

Reading the same data server-side

useTaxbit is a React hook — it only runs while the filer has a browser tab open. For lifecycle awareness when the filer isn't online (a reviewer just resolved a cure, a state change happened overnight), read the status from the server.

The endpoint is the same one the hook calls. Your backend hits it directly with the Account Owner–scoped bearer token:

GET /tax-documentation-status
Authorization: Bearer <accountOwnerToken>

The response is the snake_case server shape (w_form_questionnaire.issues[].status). Apply the same filtering logic — the field names and enum values are identical to the client shape.

Reacting to changes with webhooks

For systematic awareness without polling, subscribe to the ACCOUNT_OWNER_TAX_DOCUMENTATION_STATUS webhook. It fires on any status change to the documentation object, including:

  • Filer submits a cure → an issue flips OPENIN_REVIEW
  • Reviewer resolves a cure → IN_REVIEWRESOLVED
  • Reviewer requests additional documentation → IN_REVIEWOPEN
  • The underlying W-Form expires or has a change-in-circumstances → new issues are added

A handler should:

  1. Fetch the current status object for the account_owner_id from the payload.
  2. Diff against your last-known state.
  3. Route the right notification — email the filer when an issue is back to OPEN, notify your tax-ops team when something flipped to IN_REVIEW, update your compliance ledger on RESOLVED.
app.post('/webhooks/taxbit', async (req, res) => {
  const { event_type, account_owner_id } = req.body;

  if (event_type !== 'ACCOUNT_OWNER_TAX_DOCUMENTATION_STATUS') {
    return res.sendStatus(200);
  }

  const status = await fetchTaxDocStatus(account_owner_id);
  const issues = status?.w_form_questionnaire?.issues ?? [];

  const open     = issues.filter((i) => i.status === 'OPEN'      && CURABLE.has(i.issue_type));
  const inReview = issues.filter((i) => i.status === 'IN_REVIEW' && CURABLE.has(i.issue_type));
  const resolved = issues.filter((i) => i.status === 'RESOLVED'  && CURABLE.has(i.issue_type));

  await onStatusChange(account_owner_id, { open, inReview, resolved });
  res.sendStatus(200);
});

The webhook fires for any state change on the documentation object, not just cures. Don't trigger filer-facing notifications directly from the event — diff against your last-known state in your own database first. See the Webhooks Guide for the full event payload and retry semantics.

Quick reference

NeedSource
Should we mount the SDK widget?useTaxbit().needsCuringDocumentation
Which specific issues are open?statusData.wFormQuestionnaire.issues.filter(i => i.status === 'OPEN')
Which issues are awaiting review?…filter(i => i.status === 'IN_REVIEW')
Which issues have been resolved?…filter(i => i.status === 'RESOLVED')
Refresh after a filer submitsawait useTaxbit().refresh()
Same data outside the browserGET /tax-documentation-status server-to-server
React to state changes without pollingACCOUNT_OWNER_TAX_DOCUMENTATION_STATUS webhook

Notes on status semantics

  • OPENIN_REVIEW happens when a filer submits a cure through the SDK.
  • IN_REVIEWRESOLVED happens when your reviewer accepts the cure.
  • IN_REVIEWOPEN happens when your reviewer rejects the cure or requests additional documentation. The filer can submit again.
  • RESOLVED is terminal. The issue will not change status again.

A single documentation submission can have multiple issues, each tracked independently. Filter for status === 'OPEN' to know what's actionable at any given moment.