NodaroAccount
Nodaro Docs
DocumentationNode ReferenceModelsTemplatesAI Agents (MCP)DevelopersSelf-hostingResearchCompare
TypeScript SDK

Models and credits

List the AI models a Nodaro instance offers, read your credit balance, look up a model's price before you run it, and read plans and your subscription.

client.models returns the catalog of AI models a Nodaro instance offers, and client.credits returns your credit balance and the price of any model. client.billing returns the plans on sale and your subscription. Use them together to show users which models they can pick and what each run costs. The methods call the same endpoints as the Credits REST API. Prices and balances exist on Nodaro Cloud; self-hosted Community and Business installs have no credit system. See Credits.

Methods

MethodWhat it does
models.list(opts?)List the models, grouped by kind and maker
credits.balance()Read your credit balance and tier
credits.modelCosts(ids)Look up the credit price of up to 50 models or variants
credits.transactions(options?)Read your credit transactions, one page at a time
credits.usageSummary(options?)Sum your credit usage over a period, for a billing screen
credits.history(options?)Read every movement of your balance, one page at a time
credits.exportHistory(filters?)Export the whole filtered history as CSV
credits.claimWelcomeOffer(input?)Accept the welcome offer in a first-party web app
credits.markWelcomeOfferSeen()Record that the welcome popup was shown
billing.plans() and moreRead the plans and credit packs on sale, your subscription and how the deployment bills. In a first-party app, also the purchases, invoices, card, automatic top-up and checkout

client.models

models.list(opts?)

Returns the model catalog (GET /v1/models), grouped by kind (image, video, audio) and by maker. Each model has its capabilities, its credit prices on Nodaro Cloud, and short prompt tips. The endpoint is public, and the server caches the answer for 5 minutes. The MCP list_models tool returns the same data.

list(opts?: {
  kind?: "image" | "video" | "audio"
  mode?: string
  family?: string
  featuredOnly?: boolean
}): Promise<ModelsListResult>

Prop

Type

const catalog = await client.models.list({ kind: "video", mode: "i2v" })

for (const section of catalog.sections) {
  for (const family of section.families) {
    for (const model of family.models) {
      console.log(family.family, model.id, model.durations, model.pricing?.[0]?.credits)
    }
  }
}

The result has sections, one per kind, each with families of models; recommendations, lists of model ids for common jobs; and totalModels. Each model has these fields:

FieldTypeDescription
idstringThe model id, for the provider parameter of a node run.
label, descriptionstringThe display name and a short description.
modesstring[]What the model does, such as t2i, i2i, t2v or i2v.
useCasesstring[]Jobs the model suits.
aspectRatios, resolutions, qualities, durationsarraysThe values the model accepts, where they apply.
featuresstring[]Extra capabilities.
pricing{ identifier, credits, note? }[]The credit price of each variant: the price a run is charged, the same figure as the Run button. Nodaro Cloud only.
featuredbooleanWhether the model is featured.
promptTipsstring[]Short prompting advice for the model.
doctrineCoveredbooleantrue only when Nodaro has sourced prompting guidance for the model's family. Show a "maker guidance" badge only when it is true.
maker{ id, name }The company that made the model. id is stable, one per company, such as "google" or "black-forest-labs", so you can key your own assets on it. A newer server can add makers, so handle an id you do not know. The maker is not always the family the model is grouped under. A model that the instance offers under its own label has no maker.
logoUrlstring | nullThe maker's official mark, an SVG on the instance's own media CDN. null when the instance publishes no mark for that maker: draw the maker's name or initial instead. It is also null for a model that the instance offers under its own label. Use the address as given, because a replaced mark arrives under a new address. The MCP tool leaves it out.

An older server leaves out maker and logoUrl.

defaultDuration is the length an unset duration renders and is charged at. autoDuration means duration: -1 lets the model choose the length. referenceTaskTypes lists the values referenceTaskType takes on that model: ["reference", "edit", "extend"] on seedance-2-5, and absent elsewhere.

Size rules and file formats

A model whose sizes depend on its aspect ratio or its background carries these fields. Every other model leaves them out, and so does an older server.

FieldTypeDescription
resolutionsByAspectRatioRecord<string, string[]>The aspect ratios at which the model renders fewer sizes than resolutions, each with the sizes it renders there. A ratio not listed renders every entry of resolutions. A request for a size its ratio does not render runs at the largest size the ratio renders at or below it, and is charged at that size. adjustments reports the change.
transparentResolutionstringThe one size a transparent background renders at. A transparent request at another size runs at this size, and is charged at it.
backgroundOnlyAtTransparentResolutiontrueThe model takes a background setting only at transparentResolution. At any other size no background is sent, so opaque renders as auto.

GPT Image 2.5 (gpt-image-2-5-flare, gpt-image-2-5-sunburst and their -i2i edit models) renders only 1K at 27:16, 16:27, 9:8 and 8:9. It renders a transparent background only at 1K, and takes a background setting only there. GPT Image 2.6 Flare and Sunburst also render a transparent background only at 1K.

A model that writes more than one kind of file carries outputFormats, its default first. Where it writes a GIF or an SVG, it also carries gifFrameSize (512), gifFrames ({ min, max, default, values? }), gifFrameDurationMs ({ min, max, step, default }) and svgLevels. See Image file formats.

const { sections } = await client.models.list({ kind: "image" })
const m = sections.flatMap((s) => s.families.flatMap((f) => f.models)).find((x) => x.id === "gpt-image-2-5-flare")!
// The sizes to offer at the chosen ratio:
const sizesAt = (ratio: string) => m.resolutionsByAspectRatio?.[ratio] ?? m.resolutions ?? []
sizesAt("27:16") // ["1K"]
sizesAt("16:9")  // ["1K", "2K", "4K"]

To draw maker marks without the catalog, call GET /v1/model-makers. It answers { makers: [{ id, name, logoUrl }] }, with every maker the platform knows, and the same fields as the catalog. It is public and cached for 5 minutes. See Discover models.

for (const section of catalog.sections)
  for (const family of section.families)
    for (const m of family.models)
      console.log(m.id, m.maker?.name ?? family.family, m.logoUrl ?? "(no mark)")

The same catalog is on the Models pages. For advice on which model to use, read Choosing a model.

client.credits

credits.balance()

Returns your credit balance and tier (GET /v1/user/credits). Throws UnauthorizedError when no user is signed in.

balance(): Promise<UserBalance>
const balance = await client.credits.balance()
console.log(`${balance.total} credits (${balance.effectiveTier})`)
FieldTypeDescription
totalnumberThe credits you can spend now.
subscriptionnumberSubscription credits left this month. They reset to monthlyAllocation every month, on a yearly plan too, and unused ones do not roll over.
topupnumberCredits you bought separately.
dailySpentnumberCredits spent today.
dailyLimitnumber | nullThe daily spending limit, or null for none.
monthlyAllocationnumberCredits the plan grants every month, on monthly and yearly billing alike.
tierstringThe stored subscription tier, such as "free" or "pro".
effectiveTierstringThe tier actually applied. "payg" means pay-as-you-go: no subscription, but bought credits, with every model, no watermark and no daily limit.
featuresRecord<string, unknown>The features of the tier.
periodEndstring | nullThe end of the current billing period, as an ISO 8601 date. On a yearly plan it is a year away, but the subscription credits still reset every month.
appCreditsAllowancenumberCredits earned by running apps, on the free tier.
freeGrantStateFreeGrantStateWhere the free signup grant stands: "unclaimed", "granted", "withheld" or "revoked". "withheld" means the account works but the grant did not land, so show the activation path. Absent where the deployment has no signup grant.
freeGrantNotice"browser_unmeasured"Only with "withheld", when the claim showed no browser signals at all. Your app may say that free credits need a regular browser and a reload.
welcomeOfferWelcomeOfferStatePresent only while the deployment has the welcome offer switched on. Absent means no popup, no banner and no block. See claimWelcomeOffer().
externalWallet{ available: number | null }Present when the deployment uses a shared external wallet. null means the amount is unavailable. Do not show total in its place. See External wallet.
allowanceBalanceAllowance | nullOnly on a deployment where one billing account pays for every user: { granted, remaining, enforced }, your own allowance in credits. Show it there, because total is your personal balance, which does not fund runs on such a deployment. enforced: false means it is shown, not enforced. null for the billing account itself or when it could not be read, never 0.
displayBalanceDisplayOn a deployment that shows its own unit instead of credits: the same figures, converted by the server. Absent where no unit is configured.

Prefer effectiveTier over tier when you decide what to show. On a deployment where one billing account pays for every user, tier and effectiveTier are the deployment's plan, the billing account's tier.

BalanceDisplay holds unit (the unit's label), decimals (the places its figures carry, 0 to 4), then total, subscription, topup, dailySpent, dailyLimit, monthlyAllocation and appCreditsAllowance in that unit. Each is number | null, where null means unavailable: show an em dash, never 0. It adds allowance: { granted, remaining } | null, externalWallet: { available } and welcomeOffer: { credits }, each present exactly when the raw key is. Where the deployment names its unit per interface language, it also carries unitLabels (locale id to word): show unitLabels[locale] ?? unit. Render these figures as they are, and never convert credits yourself. display is absent for the billing account's own read on a deployment with one billing account, because its balance is the deployment's pool, kept in credits.

credits.modelCosts(ids)

Looks up the credit price of models and their variants in one call (POST /v1/credits/model-costs). It checks up to the first 50 identifiers.

modelCosts(ids: string[]): Promise<{
  data: Record<string, number>
  missing: string[]
  errors: string[]
  estimates?: string[]
  errorCodes?: Record<string, { code: string; message: string }>
  display?: ModelCostsDisplay
}>

Prop

Type

const { data, missing } = await client.credits.modelCosts([
  "nano-banana-pro",
  "nano-banana-pro:4K",
  "seedance-2-fast:8s:720p",
])
console.log(data["nano-banana-pro:4K"])
if (missing.length) console.warn("No price for:", missing)
  • data maps each priced identifier to its credit price.
  • missing lists identifiers that have no price. Show a dash for them.
  • errors lists identifiers whose lookup failed. The other prices still arrive.
  • estimates lists the identifiers in data whose figure is an estimate, not a fixed price. These are metered LLM price keys. Show them as ≈N. The field is absent when there are none.
  • errorCodes says why each identifier in errors has no price. Today the code is llm_model_unpriced: a model with no rate on the lane it would be served on, which cannot be run. The field is absent when errors is empty.
  • display is present on a deployment that shows its own unit instead of credits: { unit, decimals, data }, where data maps the same identifiers to their price in that unit, converted by the server (null means unavailable). An identifier in estimates is an estimate here too. Show unitLabels[locale] ?? unit as the unit's word when unitLabels is present. GET /v1/credits/model-cost adds display: { unit, decimals, creditCost } there. See Prices in the deployment's own unit.

The identifiers of each model are in the pricing field of models.list() and on each model's page. Settings such as quality, resolution and duration change the price, so name the variant the run will use. The price at run time is checked again when the run starts, so this call is a preview.

Speech models are priced by the length of the text. Their identifier for each started block of 100 characters is <model>:per-100-chars, for example elevenlabs-v4:per-100-chars. It lands in missing on an install that does not price speech by length, so the answer also tells you which rule the server charges by.

A request is priced as a whole and rounded once. So the exact price of a request of N started blocks is the identifier <model>:per-100-chars:<N>u, with N from 8 up to the model's cap per request. N times the block price can be higher. The same id shape prices every run charged per unit: <row>:<N>u is the exact price of N units of that row, for example apply-edl:4u, trim-video:5u, ltx-2.3-pro-retake:per-second:2.5u or elevenlabs-dubbing:3u. A whole Voice Changer Pro convert is voice-changer-pro:<slot>,<slot>,…, which voiceChangerProQuoteCreditId(slots) from @nodaro/shared builds. See Exact prices of runs priced per unit.

An LLM call has no fixed price. It is charged for the tokens it actually used, at the model's rate on the lane that served it. Its price key is <feature>:<model>, plus :direct on the provider's own API, or :effort-<level> on a model whose price depends on the Effort, and llmPriceKey from @nodaro/shared/llm-models builds it. The answer is an estimate of a typical call:

import { llmPriceKey } from "@nodaro/shared/llm-models"

const key = llmPriceKey("llm-chat", "claude-fable-5") // "llm-chat:claude-fable-5"
const { data, estimates } = await client.credits.modelCosts([key])
const shown = estimates?.includes(key) ? `~${data[key]}` : String(data[key])

A metered key in missing means the server does not price LLM calls per model, so show no price for it. GET /v1/credits/model-cost?model=<id> answers one identifier as { model, creditCost, estimate?: true, display? }. See LLM calls and How LLM calls are charged.

credits.transactions(options?)

Returns one page of your credit transactions, newest first (GET /v1/credits/transactions). Cloud edition only: a self-hosted install answers 404.

transactions(options?: { limit?: number; cursor?: string }): Promise<CreditTransactionsPage>

Prop

Type

The page is { data: CreditTransaction[], nextCursor }. nextCursor is null when there are no more rows.

FieldTypeDescription
idstringThe transaction id.
created_atstringAn ISO 8601 date.
credits_chargednumber | nullWhat the run cost once settled. It can be lower than the hold, it is 0 when the hold was refunded, and it is null while the hold is still open.
credits_reservednumber | nullThe hold taken when the run was accepted. It never changes.
credits_usednumber | nullThe same hold under its original name, kept for existing integrations. Prefer credits_charged.
actionstring | nullThe model or operation the credits were for.
statusstring | null"reserved", "committed" or "refunded".
metadataRecord<string, unknown>The run's own billing details.
payer"user" | "workspace""workspace" means a class or team budget paid, not your balance.
workspaceIdstring | nullThe paying workspace, when payer is "workspace".

Which amount to read. A run first holds credits, then it is settled: it is charged all or part of the hold, and the rest goes back to your balance. Add up credits_charged for your spend. See Credits.

let cursor: string | undefined
let spent = 0
do {
  const page = await client.credits.transactions({ limit: 50, cursor })
  for (const t of page.data) spent += t.credits_charged ?? 0
  cursor = page.nextCursor ?? undefined
} while (cursor)
console.log(`${spent} credits charged`)

credits.usageSummary(options?)

Returns the credit usage of a period, summed the way a billing screen shows it (GET /v1/credits/usage-summary). It counts your personal money only: a run a workspace paid for is not in it. Nodaro Cloud only.

usageSummary(options?: { from?: string; to?: string; bucket?: "day" | "week" | "month" }): Promise<UsageSummary>

Prop

Type

On a deployment where one billing account pays for every user, only the billing account's own session can call it. Its tokens get ForbiddenError with the code payer_balance_jwt_only.

FieldTypeDescription
period{ from, to, bucket }The period as the server took it.
totalsUsageSummaryTotals{ used, purchased, granted, refunded, runs, avgPerRun }. See below.
mostUsedUsageByModel | nullThe model with the most spend, or null with no runs.
byModelUsageByModel[]{ model, provider, credits, runs, share }, by spend. share is the part of used, from 0 to 1.
byCategoryUsageByCategory[]{ category, credits, runs }, where category is "image", "video", "audio" or "other".
overTimeUsageOverTime[]One row per bucket, empty ones included, oldest first: { bucket, used, purchased, runs }. bucket is the UTC day ("2026-10-26"), the week's Monday or the month ("2026-10"). The first bucket may be a partial week or month.
bySession, byWorkflowUsageByGroup[]What each session or workflow cost, by spend: { id, name, tool, credits, runs, models, lastAt }. name is null when the session or workflow has none or is gone. tool is on a session only.
truncatedbooleantrue when a read hit the server's cap of 5,000 rows, so the figures are too low. Ask for a shorter period.
partialbooleantrue when a side read failed, so a grouping is missing or unnamed. The totals are still whole.

In totals:

  • used adds up each run's charge. A run that is still running counts at its hold, and a refunded run does not count.
  • purchased is the packs, the loads and the subscription grants.
  • granted is the free credits, minus any grant taken back.
  • refunded is what came back for a refunded run or from Stripe. A hold released when a run settled below it is not a refund.
  • runs counts the runs charged or still running. avgPerRun is used divided by runs, with one decimal.
const s = await client.credits.usageSummary({ bucket: "week" })
console.log(s.totals.used, "credits used;", s.mostUsed?.model, "most used")
for (const w of s.overTime) console.log(w.bucket, w.used)

credits.history(options?)

Returns one page of every movement of your balance, newest first (GET /v1/credits/history): runs, purchases, grants, refunds, adjustments and expiries, each with the balance after it. transactions() lists runs alone, while history() is the whole statement a billing screen shows. Nodaro Cloud only, with the same rule as usageSummary() on a deployment with one billing account.

history(options?: CreditHistoryOptions): Promise<CreditHistoryPage>

Prop

Type

The page is { data: CreditHistoryEntry[], nextCursor }. nextCursor is null at the end.

FieldTypeDescription
idstringThe row's id.
atstringWhen it happened, as an ISO 8601 date.
kindCreditHistoryKindWhat it was. "app" is credits earned from or paid for a published app. "adjustment" is an admin's correction and anything else.
sourcestringThe record's own word, such as usage, one_time_purchase, subscription_renewal, signup_grant, refund, app_earnings or expiry.
amountnumberThe movement, signed: a run's hold is negative, a purchase positive.
balanceAfternumberThe balance right after it.
pool"subscription" | "topup" | "mixed"Which part of the balance moved.
descriptionstring | nullThe record's note.
model, providerstring | nullThe model that ran and its provider, on a row with a job.
chargenumber | nullThe job's charge, on its reserve row only, the hold taken when the run was accepted. It is the settled charge, the hold while the run is open, or 0 once refunded. Every other row has null, so adding up charge counts each job once. Read this field for spend, not amount.
statusstring | nullBeside charge: "reserved", "committed" or "refunded".
jobCreditHistoryJob | nullOn a row with a job: { id, type, status, startedAt, completedAt, durationMs, workflowId, sessionId }. type and the rest are null when the job is gone.
receiptUrlstring | nullStripe's receipt, on a purchase that has one.
const page = await client.credits.history({ kind: "usage", limit: 50 })
for (const e of page.data) console.log(e.at, e.model, e.charge ?? e.amount, "→", e.balanceAfter)

credits.exportHistory(filters?)

Returns the whole filtered history as CSV text (GET /v1/credits/history?format=csv). It has the same fields and filters as history(), one line per movement, newest first, and no paging. It holds at most 5,000 rows, and its last line says when the list was cut: narrow from and to for the rest.

exportHistory(filters?: CreditHistoryFilters): Promise<string>
const csv = await client.credits.exportHistory({ from: "2026-10-01T00:00:00Z" })
const href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }))

credits.claimWelcomeOffer(input?)

Records the user's "yes" to the welcome offer in one action (POST /v1/credits/welcome-offer/claim). It records the marketing-email consent, then claims the free signup grant with that consent. The abuse checks still decide, so read grant. An account whose grant was already decided only has its consent recorded. Nodaro Cloud only: a self-hosted instance answers 404.

claimWelcomeOffer(input?: WelcomeOfferClaimInput): Promise<WelcomeOfferClaimResult>

This method needs a signed-in browser session, from supabaseAuth(), because the consent is a person's choice. An API token or an OAuth app token gets 403. While the deployment has the offer switched off, the call answers 409 welcome_offer_disabled, so show the offer only when balance().welcomeOffer is present. On a deployment where one billing account pays for every user, free signup credits do not apply: the call answers 409 not_applicable, records nothing, not even the consent, and balance() carries no welcomeOffer.

WelcomeOfferClaimInput is the SignupKeys from collectSignupKeys() plus sourceApp, your app's slug of up to 40 lowercase letters, digits and dashes. A malformed slug is stored as absent, never refused. It also takes locale, the interface language the user answered in. That is the id of a language Nodaro ships, such as "en", "ja" or "pt-BR". It is matched exactly and case-sensitively, so "en-US" or "pt-br" is stored as absent. It is recorded with the consent as the language of the latest one.

FieldTypeDescription
consent"granted"The email consent is recorded.
grantFreeGrantState"granted": the grant landed now or earlier. "withheld": the abuse checks refused it. "revoked": the grant was taken back earlier. "unclaimed": nothing moved, so treat it as a failure and let the user retry.
creditsnumberThe grant's figure, the same as balance().welcomeOffer.credits. A grant tops the subscription credits up to this figure. It is not added on top.

WelcomeOfferState has three fields: popupSeen (the one-time popup was shown), consentPending (the account has its credits but still owes the email consent) and credits (what a claim grants). Show the credits figure in the offer instead of a fixed number, because it follows the server.

credits.markWelcomeOfferSeen()

Records that the one-time welcome popup was shown (POST /v1/credits/welcome-offer/seen), so welcomeOffer.popupSeen turns true in every app. Calling it again changes nothing. A failed call only means the popup may show once more, so you can ignore errors. The rules are those of claimWelcomeOffer(). On a deployment where one billing account pays for every user, it also answers 409 not_applicable.

markWelcomeOfferSeen(): Promise<{ ok: true }>
import { collectSignupKeys } from "@nodaro/sdk/signup-signals"

const { welcomeOffer } = await client.credits.balance()
if (welcomeOffer && !welcomeOffer.popupSeen) {
  showPopup({ credits: welcomeOffer.credits }) // the server's figure, never a constant
  void client.credits.markWelcomeOfferSeen().catch(() => {})
}

// On "Yes":
const keys = await collectSignupKeys()
const { grant, credits } = await client.credits.claimWelcomeOffer({ ...keys, sourceApp: "my-app", locale: "en" })
if (grant === "granted") showToast(`Your ${credits} free credits are ready`)

client.billing

client.billing reads the plans and prices on sale, the caller's subscription, and how the deployment bills. In a first-party app signed in with the person's own session (supabaseAuth), it also covers the whole billing screen: purchases, invoices, the card on file, the automatic top-up, and the hand-offs to Stripe Checkout and the Customer Portal. The methods marked session only answer a signed-in session alone. An API key or an app's OAuth token gets ForbiddenError, so link such a client to the app's billing page instead.

// A pricing screen: works signed out.
const { data: plans, topUps } = await client.billing.plans()

// Mark the plan the signed-in account is on.
const { effectiveTier } = await client.billing.subscription()
const current = plans.find((p) => p.id === effectiveTier) // undefined: pay-as-you-go, or a tier not on sale
MethodReturnsNotes
plans()Promise<BillingPlansResult>GET /v1/billing/plans. Public where the deployment sells to its users: no credential needed, and the same answer for every caller. Where one billing account pays for every user, only that billing account, signed in, gets the list. Returns { data: BillingPlan[], topUps, topUpRange }. Nodaro Cloud only: a self-hosted instance throws NotFoundError. A deployment that does not sell to its users throws ForbiddenError. When prices cannot be read for a moment, it throws a NodaroError with status 503.
subscription()Promise<BillingSubscriptionResult>GET /v1/billing/subscription. Returns { data: BillingSubscription | null, effectiveTier }. data is null when the account never subscribed. effectiveTier is the tier the account's entitlements follow: usually a plan id, "payg" for bought credits without a subscription, or a tier not on sale, such as "enterprise". Needs a browser session, a personal API token without a workflow limit, or an OAuth app token. Nodaro Cloud only. On a deployment where one billing account pays for every user, effectiveTier is the deployment's plan and data is your own subscription, normally null.
surface()Promise<BillingSurface>GET /v1/billing/surface. Public, on every edition. Whether the deployment meters usage at all (providerId is "none" when it does not), and the unit costs are shown in (displayUnit).
account()Promise<BillingAccountSummary | null>GET /v1/billing/account. The caller's plan, balance and dailyAllowance as the deployment's billing provider reports them, in its unit. null when the provider could not answer, which is not a zero balance. Every edition.
purchases()Promise<BillingPurchase[]>GET /v1/billing/transactions. Session only. The account's credit purchases, newest first, the latest 50: { id, stripe_transaction_id, type, amount_usd, credits_granted, tier, created_at, receipt_url }. type is "subscription", "topup" or "refund", where credits_granted is negative.
invoices()Promise<BillingDocument[]>GET /v1/billing/invoices. Session only. Every payment as a document, newest first. kind is "invoice" for a subscription payment, with Stripe's number, pdfUrl and url. It is "receipt" for a credit pack or a load, with the credits granted and the receipt as url, and no PDF. A refund is a receipt with status "refunded" and negative amount and credits. status is "paid", "open", "void", "uncollectible" or "refunded".
paymentMethod()Promise<BillingPaymentMethod>GET /v1/billing/payment-method. Session only. { card, billingDetails }: card is { brand, last4, expMonth, expYear } or null, and billingDetails is { name, email, country } or null. Both are null for an account that has not bought yet, never an error.
autoRecharge()Promise<BillingAutoRecharge>GET /v1/billing/auto-recharge. Session only. { enabled, thresholdCredits, amountUsd, failureCount, lastAttemptAt, hasSavedCard }. When the balance falls under the threshold, the amount is loaded from the saved card.
setAutoRecharge(input)Promise<BillingAutoRechargeSaved>PUT /v1/billing/auto-recharge. Session only. { enabled, thresholdCredits?, amountUsd? }. Turning it on needs both figures: thresholdCredits from 100 to 100,000, and amountUsd a whole amount within topUpRange. Otherwise it throws a NodaroError with status 400. Saving clears the failure count.
checkout(input)Promise<BillingCheckoutSession>POST /v1/billing/create-checkout-session. Session only. { priceId, mode?, returnPath?, embedded? } returns { url }, a Stripe Checkout page for a plan's price or a credit pack. Send the person there. Promotion codes apply to plans only, and a pack purchase that settles for nothing grants no credits. It throws status 409 for a plan while the account already has a subscription: use changePlan() instead. A deployment that does not sell to its users throws ForbiddenError with the code self_serve_disabled.
loadCredits(input)Promise<BillingLoadSession>POST /v1/billing/create-load-session. Session only. { amountUsd, returnPath?, embedded? } returns { url, credits }: a Checkout page that loads a whole amount within topUpRange as credits, at the packs' rate. credits is what it grants. The card is saved for the automatic top-up.
portal(input?)Promise<BillingPortalSession>POST /v1/billing/manage-subscription. Session only. { returnPath? } returns { url }, a Stripe Customer Portal page for the card, the billing address, the invoices and cancelling. It throws NotFoundError before a first purchase.
changePlan(input)Promise<BillingChangePlanResult>POST /v1/billing/change-plan. Session only. { newPriceId } returns { subscriptionId, tier }. It moves the active subscription to another plan's price. An upgrade charges the price difference for the rest of the billing period at once, and applies only when that payment succeeds. It adds the difference between the two plans' monthly credits, once per billing period, and an upgrade invoice settled for 0 adds none. A downgrade applies at once, credits the unused time on the next invoice, and keeps the current credits until the period ends. A monthly to yearly switch, or back, adds no credits. It throws NotFoundError with no active subscription, status 400 for a pack's price or the current one, and status 409 when the subscription is not active. When the upgrade waits on its payment, it throws a NodaroError with status 409 and the code plan_change_payment_required: the plan is unchanged, and details.invoiceUrl is Stripe's page for paying it. After 5 plan-change requests from one account in 24 hours, it throws RateLimitedError. See Change a plan.

returnPath is a path on your own origin, such as "/billing" or "/settings/credits?tab=history", that Stripe sends the person back to. It has one leading /, may have a query, and has no host and no #. Checkout and load add checkout=success or checkout=cancelled to its query. It wins over embedded. Without it, the Nodaro app's own pages are used.

// A billing screen in a first-party app, signed in with supabaseAuth.
const [method, docs, auto] = await Promise.all([
  client.billing.paymentMethod(),
  client.billing.invoices(),
  client.billing.autoRecharge(),
])
// Buy a pack and come back to this screen.
const { url } = await client.billing.checkout({ priceId: pack.priceId!, returnPath: "/settings/credits" })
window.location.assign(url!)

A BillingPlan has these fields:

FieldTypeDescription
idstring"free", "basic", "standard", "pro" or "business".
namestringThe display name.
prices{ monthly, yearly } | nullnull for the free plan. monthly is { priceId, amount, currency, credits }. yearly adds perMonth, the yearly amount spread over its months. priceId is the Stripe price id that checkout() and changePlan() take, and that the subscription's stripe_price_id matches. amount is in the currency's major unit (12 means 12.00), and currency is the ISO 4217 code, such as "USD". Either price may be null when it cannot be sold right now: show it as unavailable, never as free.
prices.*.credits{ amount, per } | nullWhat the price grants, per being "month" or "year". A yearly price grants the plan's credits every month, as a monthly price does.
signupCreditsnumber | nullThe free plan's one-time grant at sign-up. null on a paid plan.
parallelismnumberHow many nodes of one workflow run at once.
storageBytesnumberStorage for your media, in bytes.

topUps are the credit packs, { id, credits, price, priceId }, smallest first. id is the pack's own id, such as "topup-25". price is null when it cannot be stated right now. priceId is the Stripe price id for checkout(), and it is null with the price. A pack can always be bought as a load of price.amount through loadCredits(). topUpRange, { min, max, currency }, is the range of whole amounts that can be loaded as credits, at the same rate as the packs. See Pay as you go for buying credits without a plan.

Show models with their prices

Build a model picker for one node, with the price next to each model:

const { data: node } = await client.nodes.get("generate-video")
const { data: prices } = await client.credits.modelCosts(node.providers ?? [])

const options = (node.providers ?? []).map((id) => ({
  id,
  price: prices[id] ?? null, // null: no price on this instance
}))

Omit provider in a run to use the node's default model. When a user picks a model, send its id as provider to client.nodes.runAndWait().

Frequently asked questions

How do I list the models Nodaro offers from code?

Call client.models.list(). It returns every image, video and audio model grouped by kind and maker, with modes, aspect ratios, resolutions, durations and, on Nodaro Cloud, credit prices.

How do I find the price of one model before running it?

Call client.credits.modelCosts with the model's price identifiers, such as nano-banana-pro or nano-banana-pro:4K. The data field maps each identifier to its credit price.

How do I quote an LLM call with the SDK?

Build the price key with llmPriceKey(feature, model) from @nodaro/shared/llm-models, for example llm-chat:claude-fable-5, and pass it to client.credits.modelCosts. The figure is an estimate of a typical call, listed in estimates, so show it as ≈N.

How do I check my credit balance with the SDK?

Call client.credits.balance(). The total field is the number of credits available, split into subscription and top-up credits.

How do I add up what my runs cost with the SDK?

Page through client.credits.transactions() with each page's nextCursor and add up credits_charged. The hold a run took first, credits_reserved, is not what you paid.

How do I show Nodaro's plans and prices in my app?

Call client.billing.plans(). It needs no sign-in and returns every plan with its monthly and yearly price and credits, plus the credit packs. Buying needs the person's own signed-in session, through client.billing.checkout().

How do I build a usage or billing history screen with the SDK?

Call client.credits.usageSummary() for the totals of a period, by model, category, session and workflow. Call client.credits.history() for every movement of the balance, and exportHistory() for the same list as CSV.

Do self-hosted installs have credit prices?

No. Self-hosted Community and Business installs have no credit system, so model prices and credit costs are left out of their answers.

Last updated on

On this page