NodaroAccount
Nodaro Docs
DocumentationNode ReferenceModelsTemplatesAI Agents (MCP)DevelopersSelf-hostingResearchCompare
REST API

Credits

Read your Nodaro credit balance and history, list plans and prices, price models and runs before you start them, and learn how reservations and refunds work.

Available on Nodaro Cloud

Credits are the unit you pay with on Nodaro Cloud, and the API always reports them in credits, never in money. The credit endpoints return your balance and your credit history, price models and runs before you start them, and tell a client whether the deployment it talks to meters usage at all.

The credit endpoints exist only on Nodaro Cloud. The Community and Business editions have no credit system, so these routes answer 404 there. GET /v1/billing/surface is the exception: it answers on every edition.

Endpoints

MethodPathWhat it does
GET/v1/credits/balanceYour balance: total, subscription and top-up credits, and your tier.
GET/v1/user/creditsA fuller balance record, with daily spending.
GET/v1/credits/transactionsYour credit history, in pages.
GET/v1/credits/usage-summaryYour credit usage over a period, summed for a billing screen.
GET/v1/credits/historyEvery movement of your balance, in pages or as a CSV file.
POST/v1/credits/model-costsPublic. The credit price of up to 50 model ids.
GET/v1/credits/model-costPublic. The credit price of one model id, sent as ?model=.
POST/v1/credits/video-pro-estimateThe price of a Generate Video Pro run. See Jobs.
POST/v1/credits/seedance-ref-estimateThe price of a Seedance 2, Kling 4.0 or Kling 4.0 Flash run with reference videos. See Seedance reference-video estimates.
GET/v1/billing/plansPublic. The plans and credit packs on sale, with their prices.
GET/v1/billing/subscriptionYour own subscription and the tier your account follows.
GET/v1/billing/surfacePublic. How this deployment meters usage.
GET/v1/billing/accountYour account summary from the deployment's billing.
GET, POST, PUT/v1/billing/…Purchases, invoices, the card, the automatic top-up and checkout, for a signed-in session only. See Buy and manage billing.
POST/v1/jobs/cost-summaryThe credits of a batch of jobs.

The public routes need no token. The billing routes that buy and manage billing answer a signed-in session only. The other routes take the same tokens as every other route: a personal API token, an OAuth token or a session JWT.

Read your balance

curl -s https://app.nodaro.ai/v1/credits/balance \
  -H "Authorization: Bearer $NODARO_API_KEY"
{ "total": 1250, "subscription": 1000, "topup": 250, "tier": "pro", "effectiveTier": "pro" }
const balance = await client.credits.balance()
console.log(`${balance.total} credits (${balance.effectiveTier})`)

total is subscription plus topup. tier is the stored subscription tier, such as free or pro. effectiveTier is the tier Nodaro actually enforces: payg means no subscription but purchased credits, with every model unlocked, no watermark and no daily cap.

GET /v1/user/credits, which the SDK's client.credits.balance() reads, returns more:

FieldMeaning
total, subscription, topupYour credits, as above.
dailySpentCredits spent today.
dailyLimitYour daily spending cap, or null for no cap.
monthlyAllocationCredits your plan grants each billing cycle.
tier, effectiveTierYour stored tier and the tier enforced.
featuresThe features of your tier.
periodEndWhen the billing period ends.
appCreditsAllowanceCredits earned from app usage, on the free tier only.

Spending uses subscription credits first. Subscription credits reset to the plan's monthly allocation every month, on a yearly plan too, and unused subscription credits do not roll over. Top-up credits stay valid for 12 months from purchase.

A deployment can show its own unit instead of credits. There, GET /v1/user/credits also carries display: the same figures, converted by the server. It holds unit (the unit's label), decimals (the places its figures carry) and total, subscription, topup, dailySpent, dailyLimit, monthlyAllocation and appCreditsAllowance. It adds allowance: { granted, remaining }, externalWallet: { available } and welcomeOffer: { credits } exactly when the raw keys are present. Show these figures as they are and never convert credits yourself. null means unavailable. Where the deployment names its unit per interface language, display also carries unitLabels: { "<locale id>": "<word>" }: show the entry for your interface language, else unit. display is absent where no unit is configured, and on the billing account's own read on a deployment with one billing account, whose pool stays in credits. Every raw field is unchanged.

While the deployment runs a welcome offer, GET /v1/user/credits also carries welcomeOffer: { popupSeen, consentPending, credits }. credits is the number of credits a claim grants: show that figure rather than a fixed number. The key is absent while no offer runs. The offer is claimed in the Nodaro web app: its two routes, POST /v1/credits/welcome-offer/claim and POST /v1/credits/welcome-offer/seen, accept only a signed-in browser session and answer 403 forbidden to an API token or an OAuth app token.

How credits are charged

  • Reserved when a job starts. A generation reserves its price before it runs. When your balance cannot cover it, the call answers 402 insufficient_credits with required and, on most accounts, balance. In a workflow run, each node reserves its price when it starts, and a node that your balance cannot cover fails.
  • Charged when it delivers. The reservation becomes a charge.
  • Refunded when it does not. A generation blocked by a model's safety filter or by a deployment's policy is always refunded, and so are cancelled jobs. A metered LLM call is the exception: usage the provider already billed is charged.
  • LLM calls are metered. A language-model call holds credits first, then is charged for the tokens it used, at the model's rate. See LLM calls.
  • Priced on what actually runs. When the server corrects a setting for the chosen model, the reservation follows the corrected value. See Parameter corrections.

A job's credit_status and a transaction's status follow the same steps: reserved, then committed or refunded. So you can tell from the job itself whether its credits came back. See Credits of a job.

Transaction history

GET /v1/credits/transactions returns your credit history, newest first:

QueryMeaning
limitFrom 1 to 50. The default is 20.
cursorThe nextCursor of the previous page. It is the created_at time of the last row.
curl -s "https://app.nodaro.ai/v1/credits/transactions?limit=2" \
  -H "Authorization: Bearer $NODARO_API_KEY"
{
  "data": [
    {
      "id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "created_at": "2026-09-26T14:03:11.284Z",
      "credits_charged": 50,
      "credits_reserved": 50,
      "credits_used": 50,
      "action": "generate-image",
      "provider": "nano-banana-pro",
      "status": "committed",
      "metadata": { "model": "nano-banana-pro", "from_sub": 50, "from_topup": 0 },
      "payer": "user",
      "workspaceId": null
    }
  ],
  "nextCursor": "2026-09-26T14:03:11.284Z"
}
FieldMeaning
credits_chargedWhat the run cost you, 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 (status: "reserved"). Add up this field for your spend.
credits_reservedThe hold taken when the run was accepted. It never changes.
credits_usedThe same hold as credits_reserved, under its original name, kept for existing integrations. It never changes after a partial settle, so it reads above the charge for those runs. Prefer credits_charged.
action, providerWhat ran, and on which model.
statusreserved, committed or refunded.
payeruser for your own balance, workspace for a class or team budget.
workspaceIdThe paying workspace, or null.
metadataHow the run was billed. Always an object, {} when nothing applies.

Which amount to read. A run first holds credits, the reservation. The run is then settled: it is charged all or part of the hold, and the rest goes back to your balance. Some runs often settle below their hold, for example a hold that covers a possible re-render, a length measured after the run, or a metered run. So the hold is not what you paid. For example, a run that held 888 credits and was charged 444 reads credits_charged: 444, credits_reserved: 888, credits_used: 888 and status: "committed". The other 444 credits are back on your balance.

status is the reservation's lifecycle: reserved (credits held, not yet resolved), committed (charged, because the run delivered; the charge is credits_charged) or refunded (the reservation was released, for example after a safety block). Job payloads show the same lifecycle as credit_status, with the settled amount as credits_charged. So you do not need the transaction to know what a job cost. See Credits of a job.

metadata holds only these keys, when they apply: model, from_sub and from_topup (which of your balances paid), is_app_run, allowance_delta, web_free_mode, status, loop_trim_refunded and surround_refine_refunded.

nextCursor is null when there are no more rows.

Usage summary

GET /v1/credits/usage-summary sums the credit usage of a period, the way a billing screen shows it. It counts your personal money only: a run a workspace paid for, and an organization's credits, are another wallet. The answer is { data: UsageSummary }.

QueryMeaning
from, toAn ISO timestamp, or a date alone: 2026-10-01 is that whole UTC day. Both are inclusive. The default is the last 30 days up to now. They can be at most a year apart, or the call answers 400.
bucketday, week or month, for the series over time. The default is day.
FieldMeaning
period{ from, to, bucket }, as the server took them.
totals{ used, purchased, granted, refunded, runs, avgPerRun }. See below.
mostUsedThe first row of byModel, or null with no runs.
byModel{ model, provider, credits, runs, share } rows, by spend. share is the part of used, from 0 to 1.
byCategory{ category, credits, runs } rows, where category is image, video, audio or other.
overTimeOne row per bucket of the period, 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, byWorkflow{ id, name, tool, credits, runs, models, lastAt } rows, by spend: what each session or workflow cost. name is null when it has none or is gone, and tool is on a session only.
truncatedtrue when a read hit the server's cap of 5,000 rows, so the figures are too low. Ask for a shorter period.
partialtrue 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.

In the SDK it is client.credits.usageSummary(), and in the CLI nodaro billing summary.

Every movement of your balance

GET /v1/credits/history lists every movement of your balance, newest first, as { data: HistoryEntry[], nextCursor }: runs, purchases, grants, refunds, adjustments and expiries.

QueryMeaning
limitFrom 1 to 100. The default is 50.
cursorThe previous page's nextCursor. It is opaque: any other string answers 400.
kindusage, purchase, grant, refund, app, adjustment or expiry.
modelA model identifier, matched exactly.
from, toISO timestamps, inclusive.
qWords in the description.
formatjson, or csv for the whole filtered history as a file, at most 5,000 rows, with no paging.
FieldMeaning
id, atThe row and when it happened.
kind, sourceWhat it was. kind: "app" is credits earned from or paid for a published app, and adjustment is an admin's correction and anything else. source is the record's own word, such as usage, one_time_purchase, subscription_renewal, signup_grant, refund, app_earnings or expiry.
amountThe movement, signed: a run's hold is negative, a purchase positive.
balanceAfterThe balance right after it.
poolsubscription, topup or mixed.
descriptionThe record's note.
model, providerOn a row with a job, the model that ran.
charge, statusOn a job's reserve row only, the hold taken when the run was accepted. charge is what the run cost in the end: the settled charge, the hold while it runs, or 0 once refunded. status is reserved, committed or refunded.
jobOn a row with a job: { id, type, status, startedAt, completedAt, durationMs, workflowId, sessionId }. type and the rest are null when the job is gone.
receiptUrlStripe's receipt, on a purchase that has one.

A refund row, or a metered overrun line of the same job, has charge: null. So adding up charge over a list counts each job once. Read charge, not amount, for spend.

The CSV has the same fields, one line per movement, with numbers as numbers. Its last line says when the list was cut at 5,000 rows. In the SDK it is client.credits.history() and client.credits.exportHistory(), and in the CLI nodaro billing history, with --csv for the file.

On a deployment where one billing account pays for every user, the usage summary and the history answer the billing account's own session only, like every balance read. Its tokens get 403 payer_balance_jwt_only.

Monthly statement email

A person who turns on monthlyStatementEmail in GET / PATCH /v1/user/settings gets a short email on the 1st of each month, when they paid during the month before. It is off by default. The email lists last month's payment documents:

  • What was paid, in dollars, net of refunds.
  • How many invoices and receipts there were.
  • A link to the newest document that has one. That is the Stripe invoice's PDF, or the receipt's page, from the paid documents that GET /v1/billing/invoices lists.
  • The balance.

Months are UTC calendar months. A month with no payment sends nothing.

The statement email exists on Nodaro Cloud only, and only where the deployment has a statement email set up. A deployment without one sends nothing.

User settings

GET /v1/user/settings returns the signed-in person's own settings: { data: { tier, publicOutputs, promptTemplates, textTemplates, preferredLocale, mcpPreferences, showRecentNodes, showMostUsedNodes, variableDisplayMode, nodeDoubleClickAction, monthlyStatementEmail } }.

PATCH /v1/user/settings changes the fields you send, any of the same fields except tier, and returns them as saved.

  • promptTemplates and textTemplates replace the stored value whole. mcpPreferences is merged one axis at a time: image, video, audio and flows. A field you leave out keeps its stored value, and a field sent as null is cleared, so the MCP tools use their own default for it, for example { "mcpPreferences": { "image": { "model": null } } }.
  • publicOutputs: false needs the Standard plan or above on Nodaro Cloud. Otherwise the answer is 403.
  • An invalid value is 400 validation_error, and a patch with nothing to change is 400. A missing profile is 404.

In the SDK it is client.userSettings: get() and update(patch).

Price a run before you start it

ToolWhat it prices
POST /v1/credits/model-costsUp to 50 model ids at once.
GET /v1/credits/model-costOne model id.
GET /v1/modelsEvery model, with a pricing entry per variant. See Discover models.
GET /v1/nodes/:typeA node's creditCost, a single price or a range.
GET /v1/api/schemaA whole workflow, as estimatedCredits. See Workflows.
POST /v1/credits/estimate-workflowA workflow you send as { nodes, edges? }. It answers { data: { totalCredits, … } }.
POST /v1/credits/video-pro-estimateA Generate Video Pro run, without reserving anything. See Jobs.
POST /v1/credits/seedance-ref-estimateA Seedance 2, Kling 4.0 or Kling 4.0 Flash run with reference videos, measured like the run. See below.
POST /v1/recast/estimateA Recast run. See Recast.
POST /v1/pro-3d-render/quoteA 3D Render Pro run. See 3D scenes.

The model and node prices, and estimatedCredits, are the prices a run is charged: the same figures as the Run button in the editor. For Trim Video, Loop Video and Combine Videos, a node's creditCost is the price of one 5-second step instead, and a run costs a number of steps. In estimatedCredits, a Video SFX node counts at its 8-second price, because its clip is measured only when the run starts. The run charges the price for the measured length.

A total that holds a metered LLM node is an estimate. estimate-workflow then adds estimate: true, and GET /v1/api/schema adds estimatedCreditsIsEstimate: true. Show such a total as ≈N. It is the estimates plus the fixed prices, never a hold.

For an exact quote of a step priced by its clip's length, an upload-video node in the estimate-workflow body can carry the clip's duration (in seconds) and fps in its data, beside its url. Video Mask is then quoted at its length at that frame rate, the rule its run is charged by. This also works after a Trim Video or a Loop Video, and after a Combine Videos whose clips all declare the same frame rate.

A model's price can depend on its settings, so the ids of the variants carry them, such as nano-banana-pro:4K or gpt-image-2:2K:

curl -s -X POST https://app.nodaro.ai/v1/credits/model-costs \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"models": ["nano-banana-pro", "nano-banana-pro:4K", "gpt-image-2:2K"]}'
{
  "data": { "nano-banana-pro": 50, "nano-banana-pro:4K": 66, "gpt-image-2:2K": 42 },
  "missing": [],
  "errors": []
}
const { data, missing } = await client.credits.modelCosts(['nano-banana-pro', 'nano-banana-pro:4K'])
console.log(data['nano-banana-pro:4K'])
if (missing.length) console.warn('No price for:', missing)

Exact prices of runs priced per unit

Some runs are priced per unit, and a run is priced as a whole and rounded once. So the unit price times the number of units can come to more than the run costs. To quote such a run, ask for the identifier <row>:<N>u, the exact price of N units of that row. Both POST /v1/credits/model-costs and GET /v1/credits/model-cost answer it.

RunUnitExample id
A speech requestEach started 100 characters, from 8 up to the model's cap per requestelevenlabs-v4:per-100-chars:12u
An Apply EDL or Speaker View renderEach minuteapply-edl:4u
Trim Video, Loop Video, Combine Videos, Assemble Narrated Video and Mask CompositeEach 5 secondstrim-video:5u
An LTX 2.3 Pro extend or retakeEach second; N can be fractionalltx-2.3-pro-retake:per-second:2.5u
A dubEach started minute, 1 to 30elevenlabs-dubbing:3u, Hebrew elevenlabs-dubbing-v2:3u

A row that is not charged per unit, or a unit count that no run of it can have, lands in missing. On an install that does not price speech by length, every <model>:per-100-chars:<N>u id lands in missing too. Quote the model's flat row there.

A whole Voice Changer Pro convert has its own identifier: voice-changer-pro:<slot>,<slot>,…, with one slot per recast voice. A slot is sts-<seconds> for a speech-to-speech voice, by its stem length. A Re-speak voice is respeak-<characters> on v3, respeak-v4-<characters> on v4, and respeak-v4t-<characters> on v4 speaking a translation. For a translation, send the transcript's characters: the server adds the allowance for a re-rendered line. Use the bare kind when the measure is unknown. For example, voice-changer-pro:sts-26.76,respeak-v4-1200,respeak-v4t-800. Both endpoints answer it with the credits the convert reserves. Per-voice prices do not add up to that figure, because the convert is charged the sum, rounded once. More than 8 slots, a stem longer than an hour, or a malformed slot lands in missing. The @nodaro/shared package builds the identifier with voiceChangerProQuoteCreditId(slots), where a Re-speak slot takes an optional engine ("v3" or "v4") and a translated flag.

An id without a price lands in missing, and an id whose lookup failed lands in errors, so one bad id never fails the whole request. Show a dash for those, not zero. The prices above are an example: read the live answer, or see each model's page in Models.

In the CLI, nodaro models cost <id…> prices identifiers the same way, and nodaro models list shows each model's credit tiers.

LLM calls

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. See How LLM calls are charged.

  • The price key is <feature>:<model>, plus :direct when the call runs on the provider's own API, or :effort-<level> on a model whose price depends on the Effort. Examples are llm-chat:gemini-3.6-flash and describe-to-picker:claude-opus-5.5:direct. llmPriceKey(feature, model, reasoningEffort?, advancedMode?) from @nodaro/shared/llm-models builds the key.
  • The answer is an estimate, a typical call of that feature on that model. POST /v1/credits/model-costs lists such ids in estimates, and GET /v1/credits/model-cost adds estimate: true to its data. Show an estimate as ≈N. A batch of fixed prices only has no estimates field.
  • A model with no rate on the lane it would be served on cannot be run. The batch lists its key in errors, with errorCodes: { "<key>": { code: "llm_model_unpriced", message } }. The run itself is refused before anything is reserved. See A model with no price yet.
  • A metered key in missing means the server does not price LLM calls per model. Show no price for it.
  • A Meta Ads scrape with AI analysis holds its fixed scrape price plus one analysis call per ad. Ask for the whole analysis as one id, <analysis key>:<items>u. scrapeAnalysisQuoteId in @nodaro/shared/social builds it. The answer is the run's figure, rounded up once, so the one-call estimate times the items can come to more.
{
  "data": { "llm-chat:gemini-3.6-flash": <credits>, "nano-banana-pro": <credits> },
  "missing": [],
  "errors": [],
  "estimates": ["llm-chat:gemini-3.6-flash"]
}

Prices in the deployment's own unit

On a deployment that shows its own unit instead of credits, POST /v1/credits/model-costs also answers display: { unit, decimals, data }. data maps the same identifiers to their price in that unit, converted by the server, and null means unavailable. GET /v1/credits/model-cost adds display: { unit, decimals, creditCost } to its data. An identifier listed in estimates is an estimate in both.

  • Show these figures as they are. Do not convert credits yourself.
  • unitLabels. Where the deployment names its unit per interface language, display also carries unitLabels: { "<locale id>": "<word>" }. Show the entry for your interface language, else unit. The server does not know your interface language, so it sends the words and you choose.
  • Elsewhere display is absent, and every other field is unchanged.

Cancelling. Cancelling a metered LLM job never charges its whole hold. A job whose call has started is charged what it used, and the rest is refunded. A job cancelled before its call starts is refunded, except for what an earlier attempt of the same job already used. A single POST /v1/jobs/:id/cancel of a running job answers inFlight: true and lets it finish. See Cancel or delete a job.

Seedance reference-video estimates

A Seedance 2 run with reference videos is charged for the seconds of the reference clips plus the output seconds. A Kling 4.0 (kling-4) or Kling 4.0 Flash (kling-4-flash) run is charged for the output plus the longer of the output and the clips. Either way, the price depends on the clips' real lengths. That covers Generate Video with reference videos wired, and the Seedance 2.5 Edit choice of Video to Video, whose source clip is reference video 1. POST /v1/credits/seedance-ref-estimate measures the clips and prices them exactly as the run's reservation does. It creates no job and reserves nothing. It needs a token, and allows 60 requests a minute.

{ "provider": "seedance-2-5", "resolution": "720p", "duration": 8, "referenceVideoUrls": ["https://example.com/clip.mp4"] }
FieldTypeMeaning
providerstringseedance-2, seedance-2-fast, seedance-2-mini, seedance-2-5, kling-4 or kling-4-flash. Any other model answers 400 validation_error, with a message that names the models that qualify.
resolutionstring, optionalAs the run sends it. Omitted means the model's default.
durationnumber, optionalOutput seconds. -1 means Auto, priced as the run reserves it: the longest clip's length rounded up to a whole second, plus one second, never above the model's longest clip. While a clip is unmeasured or unknown, or when referenceTaskType is reference, Auto is priced at the model's longest clip. Omitted means the model's default.
draftboolean, optionalSeedance 2.5 Draft, priced at 480p.
referenceTaskTypestring, optionalSeedance 2.5: the reference task type the run will send, auto (default), reference, edit or extend, decided as the run decides it. reference with an Auto duration is priced at the model's longest output. edit and extend are priced at the length they run at. A clip not made yet counts as the model's longest clip. A draft is quoted as the same request at 480p. Other models ignore it, and an unknown value is ignored.
referenceVideoUrlsstring[]The reference clips the run will send, in order.
unknownReferenceVideosnumber, optionalWired clips whose file does not exist yet, because their upstream node has not made them.

The answer is { "data": { "credits", "min", "exact" } }. With every clip named, credits is exactly what the run reserves, and min equals it. A clip that cannot be measured counts as the model's longest accepted clip, as the run counts it. With unknownReferenceVideos, exact is false: credits prices each unknown clip at the longest clip the model accepts. min is the least the run can reserve, whatever length the unknown clips turn out to be. With a referenceTaskType, a longer clip can make a cheaper run, and min accounts for it. Both figures are reservations: a run whose length the model decides is charged for what it makes, which can be below min. A clip the run would refuse is refused here with the run's own 400 video_too_long. The editor shows the same range on the node while a wired clip has not been made yet.

Plans and prices

GET /v1/billing/plans lists the plans and the credit packs on sale, with what each costs. It is public: no token is needed, so a pricing page can call it before anyone signs in, and every caller gets the same answer, sent with Cache-Control: public, max-age=300. It answers whoever checkout lets through. On a deployment that does not sell to its users, only its signed-in billing account gets the list, with Cache-Control: private, max-age=300, and anyone else gets 403 self_serve_disabled. Every answer carries Vary: Authorization. It answers on Nodaro Cloud only. In the SDK it is client.billing.plans(), and in the CLI nodaro billing plans, with no sign-in needed.

{
  "data": [
    { "id": "free", "name": "Free", "prices": null,
      "signupCredits": <credits>, "parallelism": <n>, "storageBytes": <bytes> },
    { "id": "basic", "name": "Basic",
      "prices": {
        "monthly": { "priceId": "price_…", "amount": <amount>, "currency": "USD",
                     "credits": { "amount": <credits>, "per": "month" } },
        "yearly":  { "priceId": "price_…", "amount": <amount>, "currency": "USD", "perMonth": <amount>,
                     "credits": { "amount": <credits>, "per": "month" } }
      },
      "signupCredits": null, "parallelism": <n>, "storageBytes": <bytes> }
  ],
  "topUps": [
    { "id": "topup-10", "credits": <credits>, "price": { "amount": <amount>, "currency": "USD" }, "priceId": "price_…" }
  ],
  "topUpRange": { "min": <amount>, "max": <amount>, "currency": "USD" }
}
FieldMeaning
dataThe plans: the free plan first, then the paid plans from the smallest. id is stable: free, basic, standard, pro or business.
pricesnull for the free plan. Otherwise monthly and yearly: priceId, the Stripe price id that checkout and a plan change take, amount in the currency's major unit, currency as an ISO 4217 code, and yearly.perMonth, the yearly amount spread over its months. A price that cannot be sold right now is null: show it as unavailable, never as free.
prices.*.creditsWhat the price grants: { amount, per }, where per is "month" or "year". A yearly price grants the plan's credits every month, like a monthly price. null when the price does not state it.
signupCreditsThe free plan's one-time grant at sign-up. null on a paid plan, whose credits come with each price.
parallelismHow many nodes of one workflow run at the same time.
storageBytesStorage for your media, in bytes.
topUpsThe credit packs, { id, credits, price, priceId }, smallest first. Top-up credits are valid for 12 months. price is null when it cannot be stated right now, and priceId, the Stripe price id for checkout, is null with it.
topUpRange{ min, max, currency }: any whole amount in this range can be loaded as credits in the app, at the same rate as the packs.
StatusCodeWhen
403self_serve_disabledThe deployment does not sell to its users, and the caller is not its billing account.
503stripe_not_configuredThe deployment takes no card payments.
503plan_prices_unavailableThe prices could not be read. Retry shortly.

To sell a plan, send the user to the Nodaro app's pricing page, or start a checkout from a first-party app. See Buy and manage billing.

Your own plan

GET /v1/billing/subscription answers a signed-in session, a personal API token without a workflow limit, or an app's OAuth token. A workflow-limited token gets 403 forbidden. The answer is { data, effectiveTier }:

  • data is the account's latest subscription, or null when it never subscribed. It holds tier, status (active, trialing, past_due, paused, canceled, incomplete or expired), current_period_start, current_period_end, cancel_at_period_end, cancel_at and canceled_at.
  • effectiveTier is the tier the account's features follow. It is usually a plan id from GET /v1/billing/plans. It can also be "payg", for an account with no subscription and purchased credits, or a tier that is not on sale, such as "enterprise".

Match effectiveTier against data[].id from the plans list to mark the current plan. No match means pay-as-you-go or a tier that is not on sale. In the SDK it is client.billing.subscription(), and in the CLI nodaro billing subscription.

Buy and manage billing

The routes that buy and manage billing answer a signed-in session only: the Supabase JWT that a first-party web app holds. They refuse API and OAuth app tokens with 403 forbidden. A person manages billing at app.nodaro.ai/billing, or in a first-party app that calls these routes through the SDK's client.billing.

MethodPathWhat it does
POST/v1/billing/create-checkout-sessionOpens Stripe Checkout for a plan or a credit pack: { priceId, mode?, embedded?, returnPath? } returns { data: { url } }. A plan's Checkout accepts promotion codes, and a credit pack's does not. A pack purchase that settles for nothing grants no credits. It answers 409 for a plan while the account already has a subscription, active or past due. Change that plan with change-plan instead.
POST/v1/billing/create-load-sessionOpens Stripe Checkout to load a whole amount as credits: { amountUsd, embedded?, returnPath? } returns { data: { url, credits } }.
POST/v1/billing/change-planMoves an active subscription to another plan: { newPriceId } returns { data: { subscriptionId, tier } }. See Change a plan.
POST/v1/billing/manage-subscriptionA link to the Stripe Customer Portal: { returnPath? } returns { data: { url } }. It answers 404 before a first purchase.
GET/v1/billing/auto-rechargeThe automatic top-up setting: { data: { enabled, thresholdCredits, amountUsd, failureCount, lastAttemptAt, hasSavedCard } }.
PUT/v1/billing/auto-rechargeChanges it: { enabled, thresholdCredits?, amountUsd? }. Turning it on needs both figures.
GET/v1/billing/transactionsPurchase history, newest first, the latest 50: { data: [{ 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.
GET/v1/billing/payment-methodWhat is on file with Stripe: { data: { card, billingDetails } }. card is { brand, last4, expMonth, expYear } or null, and billingDetails is { name, email, country } or null. An account that has not bought yet gets both null, never an error.
GET/v1/billing/invoicesEvery payment as a document, newest first, the latest 100 of each source. See below.

Each document of GET /v1/billing/invoices is { id, kind, number, at, amount: { amount, currency }, credits, status, description, pdfUrl, url }:

  • kind: "invoice" is a subscription payment. number, pdfUrl and the hosted url are Stripe's.
  • kind: "receipt" is a credit pack or a load, with the credits granted and the hosted receipt as url, and no PDF. A refund is a receipt with status: "refunded", and its amount and credits are negative.
  • status is paid, open, void, uncollectible or refunded.
  • Every document Stripe writes, the invoice PDF, the invoice page and the receipt, is in English, whatever language the person paid in.

returnPath, on checkout, load and the portal, is a path on the calling origin that Stripe sends the person back to, such as /billing or /settings/credits?tab=history. So a first-party app on its own origin lands on its own screen. It has one leading /, may have a query, and has no host, no backslash and no #. Anything else answers 400. Checkout and load add checkout=success or checkout=cancelled to its query, and returnPath wins over embedded. Without it, the app's own /billing and /pricing pages are used.

Change a plan

POST /v1/billing/change-plan moves an active subscription to another plan. What happens depends on the direction:

  • Upgrade. The price difference for the rest of the billing period is charged at once. The new plan applies only when that payment succeeds.
  • Credits on an upgrade. The account gets the difference between the two plans' monthly credits, once per billing period. Moving down and back up within one period adds nothing more. An upgrade invoice settled for 0, for example with a 100%-off coupon or a balance that covered it, adds no credits.
  • An upgrade payment that does not go through. The plan stays as it was, and the answer is 409 with { error: { code: "plan_change_payment_required", message, invoiceUrl } }. invoiceUrl is Stripe's page for paying that invoice. The change applies if the invoice is paid before it expires. The same answer comes back while an earlier change still waits for its payment.
  • Downgrade. The unused time is credited on the next invoice. The current credits stay until the period ends.
  • Monthly to yearly, or yearly to monthly. The switch restarts the billing period but adds no credits. The next monthly credits come a month after the switch.

Other answers:

  • 409 when the subscription is not active, for example after a failed renewal payment.
  • 404 when the account has no subscription.
  • 429 after 5 plan-change requests from one account in 24 hours. Refused requests count too.

Pay-as-you-go

You do not need a subscription to use the API.

  • Any purchase activates it. Buying a credit pack, or loading any whole-dollar amount from $5 to $1,000 on the Billing page, switches the account to pay-as-you-go. Larger loads get a better rate per credit.
  • Everything unlocked. effectiveTier becomes payg: every model is available, results have no watermark, and there is no daily spending cap.
  • Credits last 12 months from purchase.
  • For the developer surfaces. Pay-as-you-go credits work through the API, the SDK, the CLI and MCP. The web editor needs a subscription, and a pay-as-you-go account that spends from it gets 403 subscription_required. Token calls never get that error.
  • Subscriptions cost less per credit at a steady volume.

Two things to know:

  • Results are public by default. Private results are a subscription feature, from the Standard plan up, so pay-as-you-go results appear in the public gallery. Jobs created through MCP are always private.
  • Media is kept while the account is active. After about 3 months with no purchase and no credit spending, files older than 60 days may be removed. Spending credits again stops the clean-up. The clean-up removes only the files a generation stored as its own result, never an input the result repeats, and never a file one of your library items still holds. When a file is removed, its link is set to null in every job result that holds it.

Purchases need the person's own signed-in session: the billing routes for checkout, loads, plan changes, auto-recharge, purchase history, invoices and the payment portal refuse API and OAuth tokens. Manage billing at app.nodaro.ai/billing, or see Buy and manage billing.

Read how a deployment meters usage

Two routes let a client build cost and usage views without assuming how the deployment bills:

GET /v1/billing/surface needs no token and returns the same answer to everyone:

{
  "data": {
    "contract": 2,
    "providerId": "…",
    "displayUnit": "credits",
    "canReport": true,
    "canQuote": true,
    "canAccount": true,
    "mountCostTab": true,
    "deploymentPayer": false
  }
}
  • displayUnit is the unit a cost view should show by default, such as credits or usd.
  • On a Community install without credits, providerId is none and mountCostTab is false: show no cost view.
  • deploymentPayer is true when one billing account pays for every user. Which account that is is never shown.

GET /v1/billing/account returns your account summary as { "data": … }. When the billing service cannot answer, data is null: show that as unavailable, never as a zero balance. The summary always has:

FieldMeaning
planYour plan, as a string. unknown is a real answer.
balanceYour balance, or null.
dailyAllowanceYour daily allowance, or null.
unitThe unit of these figures.

A deployment may add optional fields, and a client shows only the ones it receives: periodStart, generations, spent, payg, daily, reserveValue and byCategory. Money figures are { amount, currency } objects. In daily, a limit of 0 means blocked, not unlimited, and daily wins over dailyAllowance when both are present. Every null means unavailable, never zero: show a dash.

POST /v1/jobs/cost-summary totals the credits of a batch of jobs. total_credits, at the top and on each breakdown row, is a number or null, and the response names its unit and counts the jobs it could not price in unavailable. A null total means no job in the batch had a known charge. It does not mean zero.

Workspaces and shared billing

  • Workspace budgets. Work done inside an organization's workspace is paid by the workspace's budget, not your balance, and its transactions carry payer: "workspace". A run over the budget answers 402 budget_exceeded, and over your own cap in that workspace 402 member_cap_exceeded. See Workspaces and organizations.
  • Deployments with one billing account. On a deployment where one billing account pays for every user, GET /v1/user/credits adds allowance: { granted, remaining, enforced }, your own allowance in credits. enforced: false means it is shown but does not stop runs. allowance is null for the billing account itself or when it could not be read, so never read null as zero. When the deployment enforces allowances, a run over yours answers 402 user_allowance_exceeded. The billing account manages the pool from its own browser session only: its balance reads with a token answer 403 payer_balance_jwt_only. It can also turn on auto top-up for the pool and create read-only billing integration keys. See The billing account's tools.
    • The plan there is the deployment's. On the balance reads, the user settings and GET /v1/billing/subscription, tier and effectiveTier are the deployment's plan, the billing account's tier. dailyLimit is null, because no daily cap applies. GET /v1/billing/subscription still answers your own subscription as data, normally null.
    • Free signup credits do not apply there. POST /v1/credits/claim-signup-grant and the two welcome offer routes answer 409 not_applicable and record nothing, not even the consent. GET /v1/user/credits carries no welcomeOffer.
  • External wallet. See External wallet for deployments that authorize spending through their own wallet.

Frequently asked questions

How do I check my credit balance through the API?

Send GET /v1/credits/balance. It returns total, subscription and topup credits, plus your tier and effectiveTier. In the SDK, client.credits.balance() returns a fuller record that also includes your daily spending.

When are credits charged for an API generation?

Credits are reserved when the job starts and charged when it delivers. When a job is blocked by a safety filter or a policy, or cancelled before it runs, the reservation is refunded. The job's credit_status shows which happened.

Can I use the Nodaro API without a subscription?

Yes. Buying any credit pack activates pay-as-you-go, with every model unlocked, no watermark and no daily cap. Pay-as-you-go credits work through the API, the SDK, the CLI and MCP, and stay valid for 12 months.

How do I know what a run will cost before I start it?

POST /v1/credits/model-costs returns the credit price of up to 50 model ids, GET /v1/models lists every model's price tiers, and GET /v1/api/schema estimates a whole workflow. Generate Video Pro has its own estimate route.

Why is an LLM price shown as ≈N?

An LLM call is charged for the tokens it actually used, at the model's rate. Before the call, Nodaro can only estimate a typical call on that model. POST /v1/credits/model-costs lists such ids in estimates, so show them as ≈N.

Can I show Nodaro's plans and prices on my own pricing page?

Yes. GET /v1/billing/plans is public and needs no token. It lists the plans, their monthly and yearly prices, the credits each grants, and the credit packs. Buying needs the person's own signed-in session.

Do the credit endpoints work on a self-hosted install?

No. The Community and Business editions have no credit system, so the credit routes answer 404 there, and node and model descriptors leave out their credit prices. You pay the model providers directly.

Last updated on

On this page