npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@root-credit/root-sdk

v0.1.3

Published

Root Pay Payments APIs Node SDK

Readme

@root-credit/root-sdk

A tiny, deterministic JS/TS SDK for the Root Pay sandbox APIs. Built so a v0-generated demo app can wire payments correctly with one or two lines of code, instead of the AI re-inventing auth, polling, and response-envelope handling on every prompt.

  • Zero runtime dependencies. Native fetch only. Node 18+.
  • Deterministic. Auth header, base URL, error class, response unwrapping, and rail-aware polling are all baked in.
  • Server-only. The X-API-KEY must never reach the browser. Use the SDK from Server Actions, Route Handlers, RSC, or plain Node scripts.
  • Sandbox-aware. Ships the allowed test bank/card/routing numbers, the John Failed magic name, and per-rail polling defaults.
  • Coverage: subaccounts (incl. moves), payees + payee payment methods, payers + payer payment methods, payouts (incl. approve), payins, webhooks, party session tokens, treasury accounts/balances/bank-transactions/account-transfers (incl. sandbox inbound-credit simulation), plus high-level flows.payTo, flows.chargeFrom, flows.moveBetweenSubaccounts, and flows.fundSubaccountFromExistingPayer orchestrators.

Install

pnpm add @root-credit/root-sdk
# or
npm i @root-credit/root-sdk

Set environment variables in your demo's .env.local:

# Full API token (three segments). NOT a bare UUID — use the whole `token_value`:
ROOT_API_KEY=test_<token_id>_<secret>
ROOT_BASE_URL=https://api.useroot.com     # optional, this is the default
ROOT_DEFAULT_SUBACCOUNT_ID=<optional, pre-created subaccount id>

ROOT_API_KEY must match what the Root API expects: test_<uuid>_<secret> or live_<uuid>_<secret> (the value returned when you create an API token). A UUID by itself will return INVALID_TOKEN from the server.


Quick start

1. Single-call payout (most common demo flow)

import { Root } from '@root-credit/root-sdk'

const root = new Root() // reads ROOT_API_KEY + ROOT_BASE_URL from env

const { finalPayout } = await root.flows.payTo({
  payee: { name: 'Quinn Ramirez', email: '[email protected]' },
  amount_in_cents: 12_500,
  rail: 'instant_bank',
  // paymentMethod, subaccount_id, currency_code are all optional;
  // sandbox-allowed test bank defaults are used automatically.
})

console.log(finalPayout.status) // 'settled' (or 'failed' on the John Failed flow)

That single call:

  1. Reuses the payee if one already exists with the same email; otherwise creates it.
  2. Attaches a sandbox-allowed default pay-to-bank payment method.
  3. Resolves a subaccount (env → list → create) and uses it.
  4. Creates the payout with auto_approve: true.
  5. Polls GET /api/payouts/{id} with the right interval/timeout for the chosen rail until terminal.

2. Single-call payin

const { finalPayin } = await root.flows.chargeFrom({
  payer: { name: 'Acme Buyer', email: '[email protected]' },
  amount_in_cents: 99_900,
  rail: 'standard_ach', // payins only accept 'standard_ach' | 'same_day_ach'
})

2b. Subaccount move + payin without payer onboarding

When the Root payer and pay-by-bank PM already exist (typical after onboarding elsewhere), use these instead of hand-wiring payins.create + polling:

await root.flows.moveBetweenSubaccounts(
  {
    from_subaccount_id: fromId,
    to_subaccount_id: toId,
    amount_in_cents: 5_000,
  },
  { idempotencyKey: crypto.randomUUID() }, // optional; safe retries for the same logical move
)

// Requires an existing payer with a usable pay-by-bank payment method attached.
const { finalPayin } = await root.flows.fundSubaccountFromExistingPayer({
  payer_id: payerId,
  amount_in_cents: 25_000,
  rail: 'standard_ach',
  subaccount_id: subaccountId,
})

3. Resource-style usage (when you want full control)

const subaccount = await root.subaccounts.getOrCreateDefault()

const payee = await root.payees.create({ name: 'Morgan Liu', email: '[email protected]' })
const pm = await root.payees.attachPayToBank(payee.id) // sandbox defaults

const payout = await root.payouts.create({
  payee_id: payee.id,
  amount_in_cents: 5_000,
  rail: 'same_day_ach',
  subaccount_id: subaccount.id,
})

const final = await root.payouts.waitForTerminal(payout.id, {
  rail: 'same_day_ach',
  onUpdate: (snap) => console.log('status:', snap.status),
})

4. Failure simulation

import { FAILURE_SIMULATION_NAME, payeeNameForTransaction } from '@root-credit/root-sdk'

await root.flows.payTo({
  payee: { name: 'Quinn Ramirez', email: '[email protected]' },
  amount_in_cents: 1234,
  rail: 'instant_bank',
  simulateFailure: true, // swaps the payee name to 'John Failed' for this transfer
})

Next.js (App Router) integration

The SDK is server-only. Construct it inside a server module and call from Server Actions, Route Handlers, or RSC:

// app/lib/root.ts
import 'server-only'
import { Root } from '@root-credit/root-sdk'

export const root = new Root()
// app/actions.ts
'use server'
import { root } from './lib/root'

export async function runPayout(formData: FormData) {
  const amount = Number(formData.get('amount'))
  return root.flows.payTo({
    payee: { name: 'Quinn Ramirez', email: '[email protected]' },
    amount_in_cents: amount,
    rail: 'instant_bank',
  })
}

API surface (high-level)

new Root({ apiKey?, baseUrl?, timeoutMs?, maxRetries?, onRequest?, defaultHeaders? })

root.subaccounts
  .create({ name })
  .get(id)
  .update(id, { name })
  .list({ parent_account_id?, bank_config_id?, limit?, cursor?, order? })
  .move({ from_subaccount_id, to_subaccount_id, amount_in_cents, metadata? }, { idempotencyKey? })
  .getOrCreateDefault({ name?, envVar? })          // env → list → create

root.payees
  .create({ name, email, metadata? }) / .get(id) / .list(params)
  .update(id, { name?, email?, metadata? })
  .findByEmail(email)
  .attachPayToBank(id, { account_number?, routing_number?, routing_number_type? })
  .attachPushToCard(id, { card_number?, card_expiry_date? })
  .listPaymentMethods(id) / .getPaymentMethod(id, pmId)
  .setDefaultPaymentMethod(id, pmId) / .deletePaymentMethod(id, pmId)

root.payers
  .create / .get / .list / .update / .findByEmail
  .attachPayByBank(id, body?, { is_default?, subaccount_id? })
  .listPaymentMethods / .getPaymentMethod / .getDefaultPaymentMethod
  .setDefaultPaymentMethod / .deletePaymentMethod

root.payouts
  .create({ payee_id, amount_in_cents, rail?, subaccount_id?, override_default_payment_method_id?, auto_approve?=true, currency_code?='USD', metadata? })
  .get(id) / .list(params) / .approve(id) / .cancel(id)
  .waitForTerminal(id, { rail?, intervalMs?, timeoutMs?, onUpdate?, signal? })

root.payins
  .create({ payer_id, amount_in_cents, rail?='standard_ach', auto_approve?=true, ... })
  .get(id) / .list(params) / .approve(id) / .cancel(id)
  .waitForTerminal(id, { rail?, ... })

root.webhooks
  .create({ url, description? })  // returns secret_key (only on create)
  .list / .get / .toggle / .delete

root.sessionTokens
  .createForParty({ party_id, party_type? })

root.treasury   // amounts are `amount_in_minor_units`, not `amount_in_cents`
  .listAccounts({ limit?, cursor?, order? }) / .getAccount(id) / .updateAccountLabel(id, label)
  .getAccountBalance(id)
  .listBankTransactions(accountId, { limit?, cursor?, order?, transaction_date_gte?, transaction_date_lte? })
  .getBankTransaction(accountId, bankTransactionId)
  .simulateInboundCredit(accountId, { rail, amount_in_minor_units, transaction_date?, sender_info?, subaccount_id? })  // sandbox only
  .getAccountTransferRailOptions({ from_account_id, to_account_id })
  .listAccountTransfers({ limit?, cursor?, order?, account_id?, amount_in_minor_units_gte?, amount_in_minor_units_lte?, created_at_gte?, created_at_lte?, rail?, status? })
  .createAccountTransfer({ from_account_id, to_account_id, amount_in_minor_units, rail, currency_code, auto_approve, metadata? }, { idempotencyKey? })
  .getAccountTransfer(id) / .approveAccountTransfer(id)
  .waitForAccountTransfer(id, { intervalMs?, timeoutMs?, onUpdate?, signal? })

root.flows
  .payTo({ payee, amount_in_cents, rail, paymentMethod?, subaccount_id?, currency_code?, metadata?, simulateFailure?, waitForTerminal?=true, onStatus? })
  .chargeFrom({ payer, amount_in_cents, rail?, bank?, subaccount_id?, currency_code?, metadata?, simulateFailure?, waitForTerminal?=true, onStatus? })
  .moveBetweenSubaccounts({ from_subaccount_id, to_subaccount_id, amount_in_cents, metadata? }, { idempotencyKey? })
  .fundSubaccountFromExistingPayer({ payer_id, amount_in_cents, rail, subaccount_id, currency_code?, metadata?, waitForTerminal?=true, onStatus? })

root.request<T>(path, { method, body?, query?, headers?, timeoutMs?, signal? })

Treasury

root.treasury reads treasury accounts, balances, and bank-reported transaction lines, and moves funds between two treasury accounts (account transfers). It's a separate resource with no flows.* wrapper — call the methods directly on the server.

⚠️ Treasury amounts are amount_in_minor_units (cents for USD), not amount_in_cents.

import { paginate } from '@root-credit/root-sdk'

const accounts = await root.treasury.listAccounts({ limit: 50 })
const account = accounts.data[0]

const balance = await root.treasury.getAccountBalance(account.id)
//   { available_amount_in_minor_units, ledger_amount_in_minor_units, reported_at, ... }

// Bank-reported transaction lines for an account:
for await (const page of paginate((p) => root.treasury.listBankTransactions(account.id, { ...p, limit: 50 }))) {
  for (const txn of page.data) console.log(txn.direction, txn.amount_in_minor_units)
}

// Move between two accounts (auto_approve + currency_code are REQUIRED on create):
const rails = await root.treasury.getAccountTransferRailOptions({ from_account_id, to_account_id })
const transfer = await root.treasury.createAccountTransfer(
  { from_account_id, to_account_id, amount_in_minor_units: 50_000, rail: rails.supported_rails[0], currency_code: 'USD', auto_approve: true },
  { idempotencyKey: crypto.randomUUID() },
)
const final = await root.treasury.waitForAccountTransfer(transfer.id) // settled | failed

Sandbox inbound-credit simulation

Sandbox only. Simulates an inbound ACH or wire credit on a SIM treasury account. Optionally attribute the credit to a subaccount under that account via subaccount_id.

const credit = await root.treasury.simulateInboundCredit(account.id, {
  rail: 'standard_ach', // or 'wire'
  amount_in_minor_units: 100_000,
  transaction_date: '2026-07-09', // optional; defaults to current UTC date
  sender_info: 'Acme Corp',         // optional
  subaccount_id: subaccountId,      // optional; credit a subaccount instead of the main account
})
// { bank_transaction_id, amount_in_minor_units, currency_code, direction: 'credit', transaction_date }

Determinism contract

The SDK guarantees the following so the AI never has to "remember" them:

| Concern | Guarantee | |---|---| | Auth header | X-API-KEY, sourced from apiKey option or process.env.ROOT_API_KEY. Value must be the full token test_<uuid>_<secret> or live_<uuid>_<secret>, not a bare UUID. Never Authorization: Bearer …. | | Base URL | baseUrl option → process.env.ROOT_BASE_URLhttps://api.useroot.com. | | Money | Payouts, payins, and subaccount moves use amount_in_cents (integers). Treasury uses amount_in_minor_units. The SDK does not accept dollars. | | Response shape | Single objects auto-unwrap { data: … }; lists keep their { data, has_more, next_cursor } shape. | | Errors | Non-2xx → throws RootApiError with .status, .code, .body, .rawText, and a friendly message. | | Retries | 5xx and 429 → 1 retry with exponential backoff. Configurable via maxRetries. | | Network errors | Retried up to maxRetries times. | | Polling | waitForTerminal uses per-rail defaults: instant_* 1.5s × 20s, same_day_ach / standard_ach 3s × 90s, wire 5s × 180s. | | Defaults | auto_approve: true and currency_code: 'USD' on payouts/payins so demos don't stall in approval queues. | | Sandbox safety | Test bank/card/routing numbers exposed as ALLOWED_TEST_* constants. Random numbers will be rejected by sandbox. | | Failure simulation | simulateFailure: true (or payeeNameForTransaction) swaps in John Failed — the only name that triggers a sandbox failure. |


Pagination

import { paginate, collectAll } from '@root-credit/root-sdk'

for await (const page of paginate((p) => root.payouts.list({ ...p, status: 'failed' }))) {
  for (const payout of page.data) console.log(payout.id, payout.status)
}

const everyFailed = await collectAll((p) => root.payouts.list({ ...p, status: 'failed' }))

Status helpers

import { isTerminal, isSuccess, statusLabel, terminalForRail } from '@root-credit/root-sdk'

statusLabel(payout.status)        // 'Queued' | 'Sent to bank' | 'Processing' | 'Paid' | 'Failed' | 'Canceled' | 'Under review'
isTerminal(payout.status)         // true for settled / debited / failed / canceled
isSuccess(payout.status)          // true for settled / debited
terminalForRail('standard_ach')   // 'debited'
terminalForRail('instant_bank')   // 'settled'

Observability

Every HTTP call can be observed via the onRequest hook — handy for the "API timeline" panels demos like to render:

const root = new Root({
  onRequest: ({ method, path, status, durationMs }) => {
    console.log(`${method} ${path} → ${status} (${durationMs}ms)`)
  },
})

What this SDK does NOT cover (intentionally)

  • /superuser/* endpoints (operator/cookie auth — out of scope for demo apps).
  • OAuth client bootstrap and /api/api-tokens (operator-provisioned).
  • Superuser-only treasury operations (e.g. h2h processing). root.treasury covers accounts, balances, bank-reported transactions, sandbox inbound-credit simulation, and account-to-account transfers — see Treasury.
  • File-upload endpoints.

For anything not yet covered, use root.request<T>(path, opts) as an escape hatch.


License

MIT