Logic API developer guide

How the Logic API is designed to work, in detail. Use it to evaluate fit and plan an integration.

How the Logic API is designed to work, in detail. Use it to evaluate fit and plan an integration. Read the sections that apply to you, in any order.

Planned interface. The Logic API isn't available yet. The names, options and code in this guide reflect the current design and prototype, and may change. Code samples are shown in React because that's where the prototype is furthest along. The same concepts apply on iOS and Android. See Development status.

Authentication

Every request to Taxbit needs a short-lived token for the user filling in the form. In production, your server creates that token, never your frontend code:

  1. Your server uses your Taxbit credentials to create an account-owner bearer token for the signed-in user. It's the same token the current Taxbit React SDK uses.
  2. Your page fetches that token from your server and passes it as bearerToken.
  3. When it expires, fetch a new one and pass it in. The client library uses the latest value.
const { token } = await fetch('/api/taxbit-token').then((r) => r.json());

<TaxbitQuestionnaire config={{ questionnaire: 'W-9', locale: 'en-US', bearerToken: token, ... }} />

Your Taxbit client secret stays on your server. The browser only ever gets the short-lived token.

When is a form done?

Every response from Taxbit includes complete: true or false. A form marked complete: true has no errors, and Taxbit won't come back to you later with a problem about it. The client library only submits when it sees that flag, so you never need to check completeness yourself.

onSuccess fires once, with one of two results:

ResultWhat it meansWhat to do
ingestedTaxbit stored the complete form.Save document.document_id and show a confirmation.
fragmentYou asked only some questions. Those answers are valid, but the full form isn't complete yet.Save data. It's been validated, but it isn't stored at Taxbit.

If you leave out onSuccess, the client library shows its own "All done" screen. Always handle onError too. { kind: 'network' } means the connection dropped and you can retry. { kind: 'rejected' } shouldn't happen, so please report it to Taxbit.

Choosing the form

For W-forms, start with questionnaire: 'W-FORM'. You don't need to know ahead of time which form a person owes. Taxbit asks a few short questions first, such as whether they're a US person and whether they're an individual or a business, and then takes them to the right form: W-9, W-8BEN, W-8BEN-E or W-8IMY. One setup covers every user.

config={{ questionnaire: 'W-FORM', locale: 'en-US', bearerToken: token }}

Naming a specific form (optional)

questionnaire also accepts a specific form. If every user you collect from needs the same one (for example, you only collect W-9s), you can name it. Taxbit then skips the questions that form already answers.

If you collect…questionnaire
Any W-form. You don't need to know which one. RecommendedW-FORM
Only if every user needs the same form:
Only W-9s (US persons)W-9
Only W-8s (non-US persons)W-8
Only W-8BEN (non-US individuals)W-8BEN
Only W-8BEN-E (non-US entities)W-8BEN-E
Only W-8IMY (intermediaries)W-8IMY

When you name a specific form, don't also send the answers it already covers. For example, don't send "US person: yes" with W-9. Taxbit returns a 400 error if you do, so each answer has only one source.

Other documentation

Self-certification and DPS declarations aren't W-forms, so they have their own values: SELF-CERT and DPS.

In the prototype, the W-9 and self-certification paths work end to end. The W-8 paths from W-FORM, and DPS, are in development. See Development status.

Pre-fill what you know

If you already have some of the person's information, pass it in seed. The fields appear already filled in, and the user can correct them.

seed: {
  account_holder: {
    name: 'Acme LLC',
    address: { country: 'US', first_line: '1 Market St', city: 'San Francisco',
               state_or_province: 'CA', postal_code: '94105' },
  },
},

Seeded values are checked just like typed ones. The field names use the same snake_case format Taxbit stores forms in. For a returning user, prefill: 'latest' starts from their most recent submission instead, and your seed values still take priority.

Ask only some questions

Sometimes you only need part of a form, or you want to show information without letting the user change it. Three options handle this:

OptionEffect
collectShow only these parts.
skipShow everything except these parts.
lockedShow these parts, but don't let the user change them.

The parts you can name are: documentType, classification, details, residencies, address and signing.

Two common setups

Review and sign. You already have the W-9 data and only need a signature:

{ questionnaire: 'W-9', seed: { /* their data */ }, locked: ['details'], collect: ['signing'] }

Residencies only. You only need tax residencies, returned to you as a fragment:

{ questionnaire: 'SELF-CERT', seed: { account_holder: { is_individual: true } }, collect: ['residencies'] }

Use locked, not skip, for anything the user is signing. People shouldn't have to certify information they can't see.

Optional question groups

The fatca, treatyClaims and typesOfIncome options turn extra groups of questions on or off. They work the same way as in the Taxbit React SDK.

Your Taxbit plan decides what's available. These options can turn a group off for a session, but they can't turn on one your plan doesn't include. Features like real-time TIN checks turn on automatically when your plan includes them.

FATCA and treaty questions aren't working in the prototype yet.

Your own field components

To use inputs from your own design system, list them under components in the config. For example, { country: MyCountryPicker } uses your picker for every country field on every form. You can replace one field, or every field of a type:

KeyReplaces
'account_holder.tin'One specific field
'country', 'tin', 'date'Every field holding that kind of value, on every form
'select', 'text'Every dropdown or every text input

Your component receives field (what to show) and form (how to read and save the value):

export const MyCountryPicker = ({ form, field }: TaxbitFieldProps) => (
  <div>
    <Label field={field} />                                {/* Taxbit's translated label */}
    <AcmeSelect
      value={form.value(field.key)}                        {/* read */}
      onChange={(v) => form.set(field.key, v)}             {/* save */}
      onBlur={() => form.blur(field.key)}                  {/* lets Taxbit time errors */}
      options={field.options.map((o) => o.value)}          {/* ISO codes, e.g. "US" */}
    />
    <Errors form={form} field={field} />                   {/* shown at the right moment */}
  </div>
);

Your own page layout

To control what surrounds the fields (the title, the progress bar and the buttons), pass your own page component.

const AcmePage = ({ page, back, primary, children }: TaxbitPageProps) => (
  <Card>
    <h1>{page.stepTitle}</h1>
    <Progress value={page.index + 1} max={page.count} />
    {children}                              {/* the fields */}
    {back && <Button onClick={back.onClick}>{back.text}</Button>}
    <Button primary disabled={primary.busy} onClick={primary.onClick}>{primary.text}</Button>
  </Card>
);

The client library still decides what the buttons do (Next or Submit, and when to move on). Your component only decides how they look.

A fully custom flow

If you need your own navigation or step order, use the useTaxbitForm hook instead of the ready-made component. Taxbit still decides the questions, and you arrange them.

const form = useTaxbitForm('W-9', { locale: 'en-US', bearerToken: token });

<TaxbitFields form={form} section={['classification', 'details']} />
<button onClick={async () => {
  const outcome = await form.submit();
  // 'incomplete' | 'ready_to_sign' | 'fragment' | 'ingested'
}}>Continue</button>

Config options

OptionTypeRequiredDescription
questionnairestringYesWhich form to collect. See Choosing the form.
localestringYesThe language, and the date format. Include the region (en-GB, not en), because date order depends on it.
bearerTokenstringNoThe user's short-lived token. See Authentication.
seedobjectNoData you already have. See Pre-fill what you know.
prefill'latest'NoStart from the user's last submission. Off by default.
collect · skip · lockedstring[]NoShow only part of the form, or make part of it read-only. See Ask only some questions.
fatca · treatyClaims · typesOfIncomeboolean · boolean · string[]NoOptional question groups.
components · pageobject · componentNoYour own fields and page layout.
onSuccess · onError · onProgress · onSubmit · onSettledfunctionsNoCallbacks for each event. onProgress fires when the user moves between steps, which makes it a good place to save a draft.

Response fields

You only need these details if you build your own components or a custom flow.

FieldTypeDescription
completebooleanWhether the whole form is finished and valid.
fields[]Field[]The questions to show right now, in order. Each one has a key (where to save the answer), a ui_type (text, select, radio, checkbox, date…), text.label (already translated) and errors.
errors[]Error[]Every problem, each with a code (for your logic) and a text (to display). hidden: true means don't show it until the user has had a chance to answer.
steps[] · sections[]titled listsHow the fields group into pages, with a title for each.
statuses[]Status[]Messages that aren't errors. READY_TO_SIGN means everything except the signature is done.
submissionobjectOnly present when complete is true. This is the finished, signed form, and the client library submits it for you.

Base your logic on key, code and option value. Those never change. Display the text values, but don't match on them, because the wording can change.

HTTP codes

CodeMeaningFix
200Taxbit checked the answers.Read complete and errors.
201The form was stored.Save the document_id.
400The request has a mistake: an unknown option, a typo or wrong capitalization in a name, or a seeded answer that conflicts with the form choice.Read message. It names the problem.
401The token is missing or has expired.Get a new token.
429Too many requests.Wait, then retry.

Integration checklist

  • ☐ Tokens are created on your server, never in frontend code
  • ☐ locale comes from the user's settings and includes a region
  • ☐ You use W-FORM, or a specific form if you only ever collect one
  • ☐ You pre-fill data you already have
  • ☐ onSuccess saves the document_id
  • ☐ onError shows a retry message
  • ☐ Anything the user signs is visible to them (locked, not skipped)
  • ☐ Your own components call form.set and form.blur

Development status

Where the Logic API stands today. Nothing is published yet, so every item below refers to the prototype or to planned work.

Working in the prototype

  • W-9, from start to finish
  • Self-certification and residencies-only
  • React client with your own components
  • iOS client (W-9)
  • Translated text

In development

  • W-8 forms and DPS
  • FATCA and treaty questions
  • Final names and options

Planned

  • How client libraries are delivered on each platform
  • Android client
  • How teams get access, including sandbox and production environments

How design partners help

  • Tell Taxbit whether the part names (details, residencies…) match how you want to split up collection.
  • Tell Taxbit which setups you need that aren't covered here.
  • Integrate early against the development environment, and report anything that doesn't behave as described.

Next steps