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:
| Questionnaire | Use 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
RequiredLog in to the Taxbit Dashboard and navigate to Settings > Developer Settings to find your environment-specific credentials.
| Credential | Purpose |
|---|---|
client_id | Public identifier for your integration |
client_secret | Server-side only. Never expose to the browser |
tenant_id | Identifies 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
RequiredCreate 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.
| Method | Endpoint |
|---|---|
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 applicableIf 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.
| Prop | Setting |
|---|---|
questionnaire | "W-FORM" |
realTimeTinValidation | true if your tenant is configured for RTT — surfaces TIN mismatches at submission rather than at year-end. |
treatyClaims | Omit — 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.
| Prop | Setting |
|---|---|
questionnaire | "W-FORM" |
treatyClaims | true 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. |
realTimeTinValidation | No 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.
| Prop | Setting |
|---|---|
questionnaire | "SELF-CERT" |
data.accountHolder.isIndividual | true for individuals; false for entities. Entities also need selfCertificationAccountType set to "FINANCIAL_INSTITUTION", "ACTIVE_NON_FINANCIAL_ENTITY", or "PASSIVE_NON_FINANCIAL_ENTITY". |
language | Defaults 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.
| Prop | Setting |
|---|---|
questionnaire | "DPS" |
language | Defaults 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" },
},
}}
/>
| Mode | In the flow | On the review screen |
|---|---|---|
full (default) | Nothing skipped; supplied data is pre-filled | All fields editable |
skipLock | Questions with valid data are hidden | Skipped fields are locked |
skipEdit | Questions with valid data are hidden | All 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 pass | In the flow | Review (skipLock) |
|---|---|---|
| Valid value | Hidden | Locked |
| Invalid or incomplete | Shown to fix | Editable |
"" on an optional field | Hidden | Locked |
"" on a required field | Shown — never skipped on "" | Editable |
| Omitted (optional) | Hidden | Editable — generally nothing to lock |
| Omitted (required) | Shown | Editable |
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) —MISMATCHandINVALID_DATAfeed a B-notice flow; aVALID_*_MATCHclears it.realTimeTinValidationattempts the match at submission and reflects it inline.vatStatus(DPS, VIES) — resolves toVALID,INVALID,INSUFFICIENT_DATA,NOT_REQUIRED, orNON_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 Native —
react-native-webview - iOS —
WKWebView, withWKUserContentControllermessage handlers - Android —
android.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
- Component & Hook Reference — every prop, hook return value, and type
- Webhooks Guide — full event taxonomy
- Tax Documentation FAQ and Remediation FAQ — handling W-8/9 issues
Updated 7 days ago
