Integration Guide

Collect W-9, W-8 (BEN, BEN-E, IMY), MRDP (DAC7), CRS, and CARF tax documentation inside your React application with a drop-in, fully customizable component. Submitted data flows into the Taxbit platform, undergoes TIN matching where applicable, and feeds end-of-year information returns. Completed forms are available from the Taxbit Dashboard and the Tax Documentation API.

The SDK supports three questionnaire types:

QuestionnaireUse it for
Forms W-8 / W-9 (W-FORM)Collect and validate Forms W-8 and W-9 ahead of payment and reporting for 1099-B, DIV, INT, and more.
Digital Platform Seller (DPS)Satisfy MRDP obligations under DAC7 (EU), and equivalents in the UK, New Zealand, and Canada.
Self-Certification (SELF-CERT)Collect CRS, CARF, and DAC8 self-certifications based on OECD guidance for global reporting.

You'll pick the one that fits your users under Configure for your use case.

Set up locally

Render the SDK in demo mode to verify it works in your app. Demo mode requires no token and makes no requests to Taxbit.

Install the SDK

npm install @taxbit/react-sdk

Compatible with React 16–19 and TypeScript 5.

Render in demo mode

import '@taxbit/react-sdk/style/inline.css';
import { TaxbitQuestionnaire } from '@taxbit/react-sdk';

export default function App() {
  return <TaxbitQuestionnaire questionnaire="W-FORM" demoMode={true} />;
}

Set questionnaire to "DPS" or "SELF-CERT" to render the other forms. onProgress and onSubmit fire in demo mode, so you can inspect the data the SDK collects.

What you should see

The component renders the questionnaire as a multi-step form: a progress indicator, one group of questions per step, inline validation, and Back / Next / Submit controls. onProgress and onSubmit fire as the user moves through it, so you can confirm data is flowing.

The styling is Taxbit's default, not a fixed design. The form is plain semantic HTML with taxbit- class names — point it at your own stylesheet to match your product. See Style it to your brand.

If the form doesn't render, verify the stylesheet import resolved and your React version is supported. Next, connect your backend to submit real data.

Connect your backend

The SDK authenticates with a bearer token scoped to a single Account Owner — the user you're collecting documentation from. Mint the token on your server; token requests from the browser are rejected.

Gather your credentials Required

Log in to the Taxbit Dashboard and navigate to Settings > Developer Settings to find your environment-specific credentials.

CredentialPurpose
client_idPublic identifier for your integration
client_secretServer-side only. Never expose to the browser
tenant_idIdentifies your organization in Taxbit

Keep your client_secret secure. Store it in a secrets manager or environment variable. Anyone with the secret can impersonate your tenant.

Non-US tenants and proxied networks. EU-provisioned tenants pass region="EU" (the default is "US"); the server-side API host is region-specific too. Where direct access to Taxbit is restricted, route requests through your own gateway with proxyDomain, plus proxyHeaders for custom auth (your proxy must strip them before forwarding).

Create an Account Owner Required

Create an Account Owner for each user you collect from. Authorize this tenant-wide call with a tenant-scoped token from POST /v1/oauth/token. Only id and account_owner_type are required.

MethodEndpoint
POST/v1/account-owners
const aoResponse = await fetch(
  "https://api.multi1.enterprise.taxbit.com/v1/account-owners",
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${tenantToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      id: "d290f1ee-6c54-4b01-90e6-d701748f0851",  // V4 UUID recommended
      account_owner_type: "INDIVIDUAL",            // INDIVIDUAL or ENTITY
    }),
  }
);

The id you provide becomes the account_owner_id in all subsequent API calls.

Provisioning strategy. For new users, create the Account Owner during onboarding, when the user creates their account in your system — only the ID is needed. For existing users, both the API and file ingestion are available for backfilling Account Owner data.

Account creation is optional for the SDK. The SDK doesn't require an Account. You may need Accounts later for transaction reporting — see the Accounts API.

Mint an Account Owner token

Exchange your credentials for an Account Owner–scoped token and pass access_token to your client.

const { access_token } = await fetch(
  "https://api.multi1.enterprise.taxbit.com/v1/oauth/account-owner-token",
  {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type:       "client_credentials",
      client_id:        process.env.TAXBIT_CLIENT_ID,
      client_secret:    process.env.TAXBIT_CLIENT_SECRET,
      tenant_id:        process.env.TAXBIT_TENANT_ID,
      account_owner_id: accountOwnerId,
    }),
  }
).then((r) => r.json());

Token expiration

Both the tenant token and Account Owner token are valid for 24 hours. If a token expires mid-session, API calls return 401 Unauthorized. Handle this in your onError callback by requesting a fresh token from your backend and re-initializing the SDK.

Render with real data

Replace the demoMode prop with the bearerToken prop. The same component now submits to Taxbit:

<TaxbitQuestionnaire
  questionnaire="W-FORM"
  bearerToken={accountOwnerToken}
  onSuccess={handleSuccess}
/>

Submissions appear in your Taxbit Dashboard and are retrievable from the tax-documentation-status endpoint. onSuccess fires when Taxbit receives the submission, not when validation completes — see Handle validation and status.

Update your Content Security Policy If applicable

If your app uses a Content Security Policy, add the following directive to allow the SDK to reach the Taxbit API:

connect-src https://*.taxbit.com;

Without this, the SDK renders but API calls will fail — check your browser's network and console errors and your onError handler before testing in production.

Configure for your use case

With real data flowing, shape the flow to fit your users: which questionnaire you need, and how to avoid asking for data you already have.

Pick your questionnaire

Each pattern below leads with the situation that determines the configuration. If more than one applies to your user base — for example, U.S. and non-U.S. payees — render the component multiple times with different questionnaire values per Account Owner.

Paying U.S. persons subject to 1099 reporting

W-FORM. Collect Form W-9 from U.S. individuals or entities who'll receive a 1099 at year-end. Use this whenever you need a certified U.S. TIN before making a reportable payment.

PropSetting
questionnaire"W-FORM"
realTimeTinValidationtrue if your tenant is configured for RTT — surfaces TIN mismatches at submission rather than at year-end.
treatyClaimsOmit — treaty claims don't apply to W-9 filers.

Watch for: a returning tinStatus of MISMATCH or INVALID_DATA after submission — these need a B-notice flow. See TIN validation.

Paying foreign persons U.S.-source income (FDAP)

W-FORM. Collect Form W-8BEN or W-8BEN-E. If the payee may claim reduced withholding under a tax treaty, enable treatyClaims to surface the additional questions about treaty country, article, and claimed rate.

PropSetting
questionnaire"W-FORM"
treatyClaimstrue if any of your foreign payees may claim treaty benefits. Safe to leave on universally — payees not claiming a treaty move through the questions without committing to anything.
realTimeTinValidationNo effect — RTT applies only to W-9 submissions.

Watch for: treatyClaimStatus: 'INVALID' and the TREATY_COUNTRY_MISMATCH issue type both mean the treaty claim won't apply as submitted. The needsResubmission flag on wFormQuestionnaire tracks whether the user needs to return. See validation and status.

Collecting CRS, CARF, or DAC8 self-certification

SELF-CERT. Collect tax residences, citizenship, and TINs from individuals or entities on platforms subject to CRS, CARF, or DAC8 reporting. The same questionnaire serves both, with the payload shape differing by entity type.

PropSetting
questionnaire"SELF-CERT"
data.accountHolder.isIndividualtrue for individuals; false for entities. Entities also need selfCertificationAccountType set to "FINANCIAL_INSTITUTION", "ACTIVE_NON_FINANCIAL_ENTITY", or "PASSIVE_NON_FINANCIAL_ENTITY".
languageDefaults to en-GB. Set explicitly for non-English markets.

Watch for: tax residences in high-risk jurisdictions trigger an additional accountHolderTaxResidenciesConfirmation step. Controlling-person data on entity flows cannot be pre-populated — those questions are always shown, even with adaptive mode enabled.

Running a digital platform with MRDP reporting obligations

DPS. Collect seller information for DAC7 (EU) and equivalents in the UK, New Zealand, and Canada. The SDK collects tax identification, address, business registration, and (where applicable) VAT.

PropSetting
questionnaire"DPS"
languageDefaults to en-GB. Set per-seller based on locale; full EU language coverage is supported.

Watch for: VAT validation via VIES is asynchronous — vatStatus may return 'PENDING' on first submission and resolve later. See VAT validation. DPS submissions don't have a downloadable PDF — canGetDocumentUrl on the hook will be false.

Adaptive mode: skip pre-filled questions

If you already hold a user's name, address, or TIN, pass it through the data prop and set adaptiveMode so the SDK hides questions the user has effectively already answered. Adaptive evaluates each field independently: valid data removes the question; missing or invalid data surfaces it.

Modes

<TaxbitQuestionnaire
  questionnaire="W-FORM"
  adaptiveMode="skipLock"
  bearerToken={token}
  data={{
    accountHolder: {
      isUsPerson: true, usAccountType: "INDIVIDUAL",
      name: "Jane Doe", tin: "776568989",
      address: { firstLine: "123 Main St", city: "Seattle",
        stateOrProvince: "WA", postalCode: "98101", country: "US" },
    },
  }}
/>
ModeIn the flowOn the review screen
full (default)Nothing skipped; supplied data is pre-filledAll fields editable
skipLockQuestions with valid data are hiddenSkipped fields are locked
skipEditQuestions with valid data are hiddenAll fields remain editable

Use skipLock when users should change only what's strictly necessary. Use skipEdit to minimize the flow while still letting users adjust anything before submitting.

When a field is skipped

Adaptive reads three signals per field: whether it's present in data, whether the value is valid, and whether it's required for the form type.

What you passIn the flowReview (skipLock)
Valid valueHiddenLocked
Invalid or incompleteShown to fixEditable
"" on an optional fieldHiddenLocked
"" on a required fieldShown — never skipped on ""Editable
Omitted (optional)HiddenEditable — generally nothing to lock
Omitted (required)ShownEditable

Invalid or incomplete data — a partial address, an invalid U.S. postal code, a wrong date format, a malformed TIN — is always shown so the user can correct it.

Empty string vs. omitted

Pass "" only for a field you already showed the user in your own UI and they chose to leave blank; the SDK reads it as "asked and intentionally skipped" and won't ask again. This applies to optional fields only — a required field with "" is treated as missing and still shown.

Don't pass "" to shorten the flow for a field the user never saw. Some fields — a W-8 mailing address, a W-8 U.S. TIN — are optional in the SDK but should still be offered; an empty string there produces a form that's valid in the SDK but invalid under tax rules.

FTIN

Non-U.S. TINs with syntax warnings are still skipped (and locked under skipLock). Setting ftinNotLegallyRequired: true overrides the FTIN entirely — the SDK treats the field as not required.

Prefill data by form type

Adaptive reads the data prop. The fields it accepts differ by form type — expand a shape to see what you can pre-fill. Which props to set for each questionnaire is covered under Pick your questionnaire.

W-9 — Individual
{
  "accountHolder": {
    "isUsPerson": true,
    "usAccountType": "INDIVIDUAL",
    "name": "Jane Doe",
    "tin": "776568989",
      "address": {
        "firstLine": "123 Main St",
        "secondLine": "",
        "city": "Seattle",
        "stateOrProvince": "WA",
        "postalCode": "98101",
        "country": "US"
      }
  }
}
W-9 — Entity (C Corporation)
{
  "accountHolder": {
    "isUsPerson": true,
    "usAccountType": "C_CORPORATION",
    "name": "Martinez Corporation",
    "dbaName": "Martinez Solutions",
    "tin": "776568989",
      "address": {
        "firstLine": "123 Main St",
        "secondLine": "",
        "city": "Seattle",
        "stateOrProvince": "WA",
        "postalCode": "98101",
        "country": "US"
      }
  }
}
W-8BEN
{
  "accountHolder": {
    "isUsPerson": false,
    "accountOwnerType": "INDIVIDUAL",
    "name": "Maria Fernandez",
    "ftin": "A1234567",
    "address": {
      "firstLine": "45 Calle Real",
      "secondLine": "Apt 3",
      "city": "Madrid",
      "stateOrProvince": "MD",
      "postalCode": "28001",
      "country": "ES"
    },
    "mailingAddress": {
      "firstLine": "PO Box 123",
      "secondLine": "",
      "city": "Madrid",
      "stateOrProvince": "MD",
      "postalCode": "28002",
      "country": "ES"
    },
    "mailingAddressIsDifferent": true,
    "countryOfCitizenship": "ES",
    "dateOfBirth": "01/01/1990",
    "ftinNotLegallyRequired": false
  }
}
W-8BEN-E
{
  "accountHolder": {
    "isUsPerson": false,
    "accountOwnerType": "ENTITY",
    "countryOfCitizenship": "US",
    "foreignAccountType": "CORPORATION",
    "name": "Fernandez Consulting Group",
    "ftin": "B9876543",
    "tin": "",
    "address": {
      "firstLine": "100 Business Rd",
      "secondLine": "Suite 500",
      "city": "Barcelona",
      "stateOrProvince": "BC",
      "postalCode": "08001",
      "country": "ES"
    },
    "mailingAddress": {},
    "mailingAddressIsDifferent": false,
    "ftinNotLegallyRequired": false
  }
}
Self-Certification — Individual
{
  "accountHolder": {
    "isIndividual": true,
    "name": "Ray Holt",
    "address": {
      "firstLine": "100 Harbour Road",
      "secondLine": "",
      "city": "Dublin",
      "stateOrProvince": "",
      "postalCode": "D02",
      "country": "IE"
    },
    "countryOfCitizenship": "IE",
    "dateOfBirth": "05/31/1994",
    "taxResidences": [
      { "country": "IE", "tin": "2342344T" }
    ]
  }
}
Self-Certification — Managed Investment Entity

Controlling-person data cannot be pre-populated at this time.

{
  "accountHolder": {
    "isIndividual": false,
    "selfCertificationAccountType": "FINANCIAL_INSTITUTION",
    "financialInstitutionType": "INVESTMENT_ENTITY",
    "investmentEntityManaged": true,
    "entityType": "TRUST",
    "name": "Managed Investments Ltd.",
    "address": {
      "firstLine": "100 Harbour Road",
      "secondLine": "",
      "city": "Dublin",
      "stateOrProvince": "",
      "postalCode": "D02",
      "country": "IE"
    },
    "countryOfCitizenship": "IE",
    "taxResidences": [
      { "country": "IE", "tin": "2342344T" },
      { "country": "AU", "tin": "51824753556" }
    ]
  }
}

Handle validation and status

TIN matching with the IRS, VAT validation via VIES, and downstream changes all resolve after the user submits.

Received vs. validated

onSuccess is the callback the SDK fires once a submission reaches Taxbit and is accepted. It confirms receipt — not that the TIN matched, the VAT validated, or the form is final.

Validation continues after the user leaves:

  • W-9 — TIN matched against the IRS
  • DPS — VAT validated against VIES

Either can take minutes to hours, so don't mark a form "complete" on onSuccess alone. Read the final result from tinStatus / vatStatus, covered next.

Read status with the useTaxbit hook

import { useTaxbit } from '@taxbit/react-sdk';

const { statusData } = useTaxbit({ bearerToken, questionnaire: 'W-FORM' });

if (statusData?.wFormQuestionnaire?.tinStatus === 'PENDING') {
  return <Badge>Validation in progress</Badge>;
}

Both fields begin PENDING and resolve after submission:

  • tinStatus (W-9, IRS match) — MISMATCH and INVALID_DATA feed a B-notice flow; a VALID_*_MATCH clears it. realTimeTinValidation attempts the match at submission and reflects it inline.
  • vatStatus (DPS, VIES) — resolves to VALID, INVALID, INSUFFICIENT_DATA, NOT_REQUIRED, or NON_EU.

See the reference for every value.

Respond to changes with webhooks

Submissions can change state long after the user is gone — a W-8 may need resubmission after a change in circumstances, a TIN may flip from VALID to MISMATCH on a re-check, an issue may be cured in the Taxbit Dashboard. These surface as webhook events.

Use the Webhooks Guide for the full event taxonomy. A few notes specific to SDK integrations:

  • The account-owner status webhook fires for any change to the status object, not just user-initiated submissions — don't trigger user-facing notifications directly from it.
  • To detect a genuinely new submission, compare the document ID from the status API: a changed ID means new data; the same ID means existing data was re-evaluated.
  • Delivery retries; if it ultimately fails, file a support ticket to redrive events, and keep your own monitoring on top of Taxbit's.

When needsResubmission is set, route the user back to the same component with adaptiveMode="skipEdit".

Offer a PDF download on the confirmation screen

This sits at the end of the flow — the confirmation screen you render after onSuccess, not your styling layer. Available for W-Form and Self-Certification submissions, not DPS.

useTaxbit mints a short-lived download URL; gate the link on canGetDocumentUrl.

const { canGetDocumentUrl, generateDocumentUrl, documentUrl } =
  useTaxbit({ bearerToken, questionnaire });

useEffect(() => { if (canGetDocumentUrl) generateDocumentUrl(); }, [canGetDocumentUrl]);

return documentUrl ? <a href={documentUrl} download>Download form</a> : null;

Customize and ship

Style it to your brand

Semantic HTML with namespaced classes. Import a stylesheet and override what you need.

import '@taxbit/react-sdk/style/inline.css'; // or 'basic.css' / 'minimal.css'
.taxbit-button { background: #0B1B33; border-radius: 8px; }
.taxbit-input:focus { outline: 2px solid #2EE5C8; }

Support mobile

There's no native SDK — render in a webview and bridge callbacks to native code, reusing the same token flow. Host a page that posts each callback to whichever bridge is present:

function notifyNative(event, payload) {
  const msg = JSON.stringify({ event, payload });
  window.ReactNativeWebView?.postMessage(msg);              // React Native
  window.webkit?.messageHandlers?.taxbit?.postMessage(msg); // iOS WKWebView
  window.TaxbitBridge?.postMessage(msg);                    // Android interface
}

<TaxbitQuestionnaire
  questionnaire="W-FORM"
  bearerToken={bearerToken}
  onSuccess={(d) => notifyNative('success', d)}
  onError={(e) => notifyNative('error', e)}
/>

Load that page in your platform's webview:

  • React Nativereact-native-webview
  • iOSWKWebView, with WKUserContentController message handlers
  • Androidandroid.webkit.WebView, with a JavaScript interface

For production:

  • Pre-warm the webview off-screen
  • Enable DOM storage
  • Handle load errors with a native offline screen
  • Set the viewport meta tag:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

Languages

Set language to pre-select the locale; account owners can also switch via an in-form dropdown (hideable with CSS). The language prop accepts the full Locale set (53 tags); the in-form picker shows a subset — roughly 45 for DPS and Self-Certification and 19 for W-Form. See Supported languages for the full list of locale codes.

Next steps