Embed Bison JIB onboarding and banking into your own app. Three layers, use any of them:
- Core — typed async API functions over a swappable transport (browser + Node)
- Validation — the exact onboarding validation rules the Bison platform uses (zod)
- Components — unstyled onboarding + banking web components (framework-agnostic)
BISON_API_KEY=your_publishable_key npx bison-jib-sdk initThis installs bison-jib-sdk and creates bison.setup.mjs. Import that file once;
the production API URL, API-key header, client, and component registration are fixed
inside the SDK.
Browser API keys are visible to users. Only use a publishable, origin-restricted embeddable key; never ship a server secret.
Import the generated setup, then call the exported functions directly:
import './bison.setup.mjs'
import { getOnboardingStatus } from 'bison-jib-sdk'
const status = await getOnboardingStatus(scope)Without the generator, the equivalent setup is one call:
import { setupBison } from 'bison-jib-sdk'
setupBison(import.meta.env.PUBLIC_BISON_API_KEY)Requests use the fixed production origin and send X-Embeddable-Key on every call.
One scope value threads through every call and picks the API route family:
type Scope = {
persona: 'wio' | 'operator'
id: string // WIO id or Operator id
entityId?: string // WIO sub-entity route family (ignored for operator)
}{ persona: 'wio', id }→/api/wios/{id}/…{ persona: 'operator', id }→/api/operators/{id}/…{ persona: 'wio', id, entityId }→/api/wios/{id}/entities/{entityId}/…
Onboarding is five sections in canonical order — business, officer, owners,
volume, documents. Read status, then submit sections one at a time:
import {
getOnboardingIndustries,
getOnboardingSection,
getOnboardingStatus,
getOnboardingTermsToken,
saveOnboardingPaymentMethods,
submitOnboardingSection,
uploadOnboardingDocument,
} from 'bison-jib-sdk'
const status = await getOnboardingStatus(scope)
const business = await getOnboardingSection(scope, 'business')
// Where should a returning user resume? (pure, from status alone)
import { resolveOnboardingResumeStep } from 'bison-jib-sdk'
const step = resolveOnboardingResumeStep(status)
// Submit a section — discriminated by `step`
await submitOnboardingSection(scope, { step: 'business', data: businessProfile })
await submitOnboardingSection(scope, { step: 'officer', data: officer })
await submitOnboardingSection(scope, { step: 'owners', data: owners, noOwnersAbove25: false })
await submitOnboardingSection(scope, { step: 'volume', data: volume })
// Documents (multipart) + Moov helpers
await uploadOnboardingDocument(scope, file, 'merchant_underwriting')
await getOnboardingIndustries(scope)
await getOnboardingTermsToken()
await saveOnboardingPaymentMethods(scope, ['cards', 'ach'])submitOnboardingSection with step: 'business' creates the provider account on first save and
returns moovAccountId on the result.
import {
completeBankAccountVerification,
deleteBankAccount,
getBankAccounts,
getPlaidLinkToken,
initiateBankAccountVerification,
registerBankAccount,
setDefaultBankAccount,
} from 'bison-jib-sdk'
await getBankAccounts(scope) // BankAccount[]
await registerBankAccount(scope, { method: 'manual', holderName: 'Acme LLC',
routingNumber: '021000021', accountNumber: '1234567890' }) // -> BankAccount
await getPlaidLinkToken(scope) // { linkToken }
await registerBankAccount(scope, { method: 'plaid', publicToken, accountId })
await initiateBankAccountVerification(scope, bankAccountId)
await completeBankAccountVerification(scope, bankAccountId, { code: 'MV1234' })
await setDefaultBankAccount(scope, bankAccountId)
await deleteBankAccount(scope, bankAccountId)Note the client-side guards the platform enforces (reproduce them in custom flows):
can't delete the default or the last account; verification is Moov-only
(Plaid accounts are born verified). The verify code is MV#### — the two
micro-deposit amounts concatenated.
All failures throw BisonApiError with status, message, errors[], and a
machine-readable errorCode when the API provides one (e.g. NON_US_ADDRESS).
For custom transports and tests, createClient() remains as an advanced escape hatch:
import { createClient, mock } from 'bison-jib-sdk'
const bison = createClient({ transport: mock() })The demo/ pages run entirely on mock() — see below.
Pure functions and zod schemas, identical to the platform app's rules. Validate your own custom forms before calling the core functions.
import { businessProfileSchema, controlOfficerSchema, validateForm } from 'bison-jib-sdk/validation'
const errors = validateForm(businessProfileSchema, formData) // { fieldName: message } or {}Light-DOM web components, zero CSS shipped by default. Everything renders into regular DOM, so plain CSS reaches all depths.
<script type="module">
import './bison.setup.mjs'
const onboarding = document.querySelector('bison-onboarding')
const bankAccounts = document.querySelector('bison-bank-accounts')
</script>
<bison-onboarding persona="wio" scope-id="wio_123"></bison-onboarding>
<bison-bank-accounts persona="wio" scope-id="wio_123"></bison-bank-accounts><bison-onboarding>— the full multi-step flow (persona,scope-id,entity-id?, optionalprefillJSON attribute or.prefillproperty keyed by section, optionallabelsJSON attribute or.labelsproperty to relabel section titles and accessible state descriptions).<bison-onboarding-partial>— an unstyled partial onboarding form covering contact, incorporation, leadership, beneficial ownership, and payment services consent. New entities submit business + embedded control officer, then owners; existing provider entities update the officer between those requests. Its built-in Banking section lets the WIO submit one manual destination account for the operator’s use; it never lists existing accounts. Setterms-urlto a Bison-hosted disclosure URL.bison-partial-completefires after onboarding succeeds.<bison-bank-accounts>— bank-account list / add / verify / set-default / delete.
Events (all bubble, payload in detail):
bison-step-change, bison-status-checked, bison-already-onboarded,
bison-already-registered, bison-submit-success, bison-submit-error,
bison-partial-complete,
bison-bank-added, bison-bank-deleted, bison-bank-default-changed,
bison-bank-verified.
The components expose a semver-governed styling contract in four layers:
- Classes — BEM-ish
bison-*hooks on every node (bison-onboarding__form,bison-field__input,bison-bank-accounts__row, …). - State attributes —
data-state="active|done|locked|error",data-step,data-provider, anddata-verified/data-defaulton bank rows. - Slots — project your own markup via
slot="header",slot="section-intro:business",slot="actions",slot="done",slot="empty-state". - Design tokens —
--bison-*custom properties.
Style them from your own stylesheet, or opt into the starter stylesheet — a clean, accessible, theme-aware default built entirely on the contract above:
import 'bison-jib-sdk/styles.css'/* Reskin the whole thing by reassigning tokens — no selectors needed. */
:root { --bison-accent: #0d7a6f; --bison-radius: 12px; }The starter sheet ships nothing unless you import it, does light + dark
(prefers-color-scheme + data-theme), and is fully overridable. The complete
class map, data-attribute map, slot map, token list, event map, and per-item semver
policy live in docs/STYLING.md.
Two pages under demo/, both driven by mock() (no backend):
demo/index.html— unstyled, proving the zero-CSS default, with a persona toggle, both components, and a livebison-*event log.demo/styled.html— the same flow withstyles.csslinked, token overrides, and aslot=example — all four styling layers visible at once.
bun run build # compile src -> dist (incl. dist/styles.css)
bunx serve . # or: python3 -m http.server
# open /demo/ (index.html) and /demo/styled.html- Amount conventions:
averageMonthlyDollarVolumeis whole dollars;averageIndividualTransactionSize/maximumIndividualTransactionSizeare dollars per the KYB DTO. The components collect dollars and convert for you. - EIN / SSN are pass-through — sent to Moov, never stored by Bison; GETs return
taxIdProvided/birthDateProvidedbooleans instead. - Beneficial-owner minimum ownership is 25% (frontend/Moov rule).
bun install
bun run build # tsc -> dist/, then copies src/styles.css -> dist/styles.css
bun test