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
| Method | Path | What it does |
|---|---|---|
GET | /v1/credits/balance | Your balance: total, subscription and top-up credits, and your tier. |
GET | /v1/user/credits | A fuller balance record, with daily spending. |
GET | /v1/credits/transactions | Your credit history, in pages. |
GET | /v1/credits/usage-summary | Your credit usage over a period, summed for a billing screen. |
GET | /v1/credits/history | Every movement of your balance, in pages or as a CSV file. |
POST | /v1/credits/model-costs | Public. The credit price of up to 50 model ids. |
GET | /v1/credits/model-cost | Public. The credit price of one model id, sent as ?model=. |
POST | /v1/credits/video-pro-estimate | The price of a Generate Video Pro run. See Jobs. |
POST | /v1/credits/seedance-ref-estimate | The 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/plans | Public. The plans and credit packs on sale, with their prices. |
GET | /v1/billing/subscription | Your own subscription and the tier your account follows. |
GET | /v1/billing/surface | Public. How this deployment meters usage. |
GET | /v1/billing/account | Your 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-summary | The 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:
| Field | Meaning |
|---|---|
total, subscription, topup | Your credits, as above. |
dailySpent | Credits spent today. |
dailyLimit | Your daily spending cap, or null for no cap. |
monthlyAllocation | Credits your plan grants each billing cycle. |
tier, effectiveTier | Your stored tier and the tier enforced. |
features | The features of your tier. |
periodEnd | When the billing period ends. |
appCreditsAllowance | Credits 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_creditswithrequiredand, 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:
| Query | Meaning |
|---|---|
limit | From 1 to 50. The default is 20. |
cursor | The 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"
}| Field | Meaning |
|---|---|
credits_charged | What 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_reserved | The hold taken when the run was accepted. It never changes. |
credits_used | The 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, provider | What ran, and on which model. |
status | reserved, committed or refunded. |
payer | user for your own balance, workspace for a class or team budget. |
workspaceId | The paying workspace, or null. |
metadata | How 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 }.
| Query | Meaning |
|---|---|
from, to | An 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. |
bucket | day, week or month, for the series over time. The default is day. |
| Field | Meaning |
|---|---|
period | { from, to, bucket }, as the server took them. |
totals | { used, purchased, granted, refunded, runs, avgPerRun }. See below. |
mostUsed | The 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. |
overTime | One 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. |
truncated | true when a read hit the server's cap of 5,000 rows, so the figures are too low. Ask for a shorter period. |
partial | true 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.
| Query | Meaning |
|---|---|
limit | From 1 to 100. The default is 50. |
cursor | The previous page's nextCursor. It is opaque: any other string answers 400. |
kind | usage, purchase, grant, refund, app, adjustment or expiry. |
model | A model identifier, matched exactly. |
from, to | ISO timestamps, inclusive. |
q | Words in the description. |
format | json, or csv for the whole filtered history as a file, at most 5,000 rows, with no paging. |
| Field | Meaning |
|---|---|
id, at | The row and when it happened. |
kind, source | What 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. |
amount | The movement, signed: a run's hold is negative, a purchase positive. |
balanceAfter | The balance right after it. |
pool | subscription, topup or mixed. |
description | The record's note. |
model, provider | On a row with a job, the model that ran. |
charge, status | On 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. |
job | On a row with a job: { id, type, status, startedAt, completedAt, durationMs, workflowId, sessionId }. type and the rest are null when the job is gone. |
receiptUrl | Stripe'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/invoiceslists. - 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.
promptTemplatesandtextTemplatesreplace the stored value whole.mcpPreferencesis merged one axis at a time:image,video,audioandflows. A field you leave out keeps its stored value, and a field sent asnullis cleared, so the MCP tools use their own default for it, for example{ "mcpPreferences": { "image": { "model": null } } }.publicOutputs: falseneeds the Standard plan or above on Nodaro Cloud. Otherwise the answer is403.- An invalid value is
400 validation_error, and a patch with nothing to change is400. A missing profile is404.
In the SDK it is client.userSettings: get() and update(patch).
Price a run before you start it
| Tool | What it prices |
|---|---|
POST /v1/credits/model-costs | Up to 50 model ids at once. |
GET /v1/credits/model-cost | One model id. |
GET /v1/models | Every model, with a pricing entry per variant. See Discover models. |
GET /v1/nodes/:type | A node's creditCost, a single price or a range. |
GET /v1/api/schema | A whole workflow, as estimatedCredits. See Workflows. |
POST /v1/credits/estimate-workflow | A workflow you send as { nodes, edges? }. It answers { data: { totalCredits, … } }. |
POST /v1/credits/video-pro-estimate | A Generate Video Pro run, without reserving anything. See Jobs. |
POST /v1/credits/seedance-ref-estimate | A Seedance 2, Kling 4.0 or Kling 4.0 Flash run with reference videos, measured like the run. See below. |
POST /v1/recast/estimate | A Recast run. See Recast. |
POST /v1/pro-3d-render/quote | A 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.
| Run | Unit | Example id |
|---|---|---|
| A speech request | Each started 100 characters, from 8 up to the model's cap per request | elevenlabs-v4:per-100-chars:12u |
| An Apply EDL or Speaker View render | Each minute | apply-edl:4u |
| Trim Video, Loop Video, Combine Videos, Assemble Narrated Video and Mask Composite | Each 5 seconds | trim-video:5u |
| An LTX 2.3 Pro extend or retake | Each second; N can be fractional | ltx-2.3-pro-retake:per-second:2.5u |
| A dub | Each started minute, 1 to 30 | elevenlabs-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:directwhen the call runs on the provider's own API, or:effort-<level>on a model whose price depends on the Effort. Examples arellm-chat:gemini-3.6-flashanddescribe-to-picker:claude-opus-5.5:direct.llmPriceKey(feature, model, reasoningEffort?, advancedMode?)from@nodaro/shared/llm-modelsbuilds the key. - The answer is an estimate, a typical call of that feature on that model.
POST /v1/credits/model-costslists such ids inestimates, andGET /v1/credits/model-costaddsestimate: trueto itsdata. Show an estimate as≈N. A batch of fixed prices only has noestimatesfield. - A model with no rate on the lane it would be served on cannot be run. The batch lists its key in
errors, witherrorCodes: { "<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
missingmeans 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.scrapeAnalysisQuoteIdin@nodaro/shared/socialbuilds 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,displayalso carriesunitLabels: { "<locale id>": "<word>" }. Show the entry for your interface language, elseunit. The server does not know your interface language, so it sends the words and you choose.- Elsewhere
displayis 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"] }| Field | Type | Meaning |
|---|---|---|
provider | string | seedance-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. |
resolution | string, optional | As the run sends it. Omitted means the model's default. |
duration | number, optional | Output 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. |
draft | boolean, optional | Seedance 2.5 Draft, priced at 480p. |
referenceTaskType | string, optional | Seedance 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. |
referenceVideoUrls | string[] | The reference clips the run will send, in order. |
unknownReferenceVideos | number, optional | Wired 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" }
}| Field | Meaning |
|---|---|
data | The plans: the free plan first, then the paid plans from the smallest. id is stable: free, basic, standard, pro or business. |
prices | null 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.*.credits | What 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. |
signupCredits | The free plan's one-time grant at sign-up. null on a paid plan, whose credits come with each price. |
parallelism | How many nodes of one workflow run at the same time. |
storageBytes | Storage for your media, in bytes. |
topUps | The 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. |
| Status | Code | When |
|---|---|---|
403 | self_serve_disabled | The deployment does not sell to its users, and the caller is not its billing account. |
503 | stripe_not_configured | The deployment takes no card payments. |
503 | plan_prices_unavailable | The 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 }:
datais the account's latest subscription, ornullwhen it never subscribed. It holdstier,status(active,trialing,past_due,paused,canceled,incompleteorexpired),current_period_start,current_period_end,cancel_at_period_end,cancel_atandcanceled_at.effectiveTieris the tier the account's features follow. It is usually a planidfromGET /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.
| Method | Path | What it does |
|---|---|---|
POST | /v1/billing/create-checkout-session | Opens 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-session | Opens Stripe Checkout to load a whole amount as credits: { amountUsd, embedded?, returnPath? } returns { data: { url, credits } }. |
POST | /v1/billing/change-plan | Moves an active subscription to another plan: { newPriceId } returns { data: { subscriptionId, tier } }. See Change a plan. |
POST | /v1/billing/manage-subscription | A link to the Stripe Customer Portal: { returnPath? } returns { data: { url } }. It answers 404 before a first purchase. |
GET | /v1/billing/auto-recharge | The automatic top-up setting: { data: { enabled, thresholdCredits, amountUsd, failureCount, lastAttemptAt, hasSavedCard } }. |
PUT | /v1/billing/auto-recharge | Changes it: { enabled, thresholdCredits?, amountUsd? }. Turning it on needs both figures. |
GET | /v1/billing/transactions | Purchase 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-method | What 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/invoices | Every 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,pdfUrland the hostedurlare Stripe's.kind: "receipt"is a credit pack or a load, with thecreditsgranted and the hosted receipt asurl, and no PDF. A refund is a receipt withstatus: "refunded", and itsamountandcreditsare negative.statusispaid,open,void,uncollectibleorrefunded.- 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
409with{ error: { code: "plan_change_payment_required", message, invoiceUrl } }.invoiceUrlis 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:
409when the subscription is not active, for example after a failed renewal payment.404when the account has no subscription.429after 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.
effectiveTierbecomespayg: 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
nullin 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
}
}displayUnitis the unit a cost view should show by default, such ascreditsorusd.- On a Community install without credits,
providerIdisnoneandmountCostTabisfalse: show no cost view. deploymentPayeristruewhen 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:
| Field | Meaning |
|---|---|
plan | Your plan, as a string. unknown is a real answer. |
balance | Your balance, or null. |
dailyAllowance | Your daily allowance, or null. |
unit | The 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 answers402 budget_exceeded, and over your own cap in that workspace402 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/creditsaddsallowance: { granted, remaining, enforced }, your own allowance in credits.enforced: falsemeans it is shown but does not stop runs.allowanceisnullfor the billing account itself or when it could not be read, so never readnullas zero. When the deployment enforces allowances, a run over yours answers402 user_allowance_exceeded. The billing account manages the pool from its own browser session only: its balance reads with a token answer403 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,tierandeffectiveTierare the deployment's plan, the billing account's tier.dailyLimitisnull, because no daily cap applies.GET /v1/billing/subscriptionstill answers your own subscription asdata, normallynull. - Free signup credits do not apply there.
POST /v1/credits/claim-signup-grantand the two welcome offer routes answer409 not_applicableand record nothing, not even the consent.GET /v1/user/creditscarries nowelcomeOffer.
- The plan there is the deployment's. On the balance reads, the user settings and
- 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.
Related
Credits
Jobs
Errors
Workspaces and organizations
AI models in Nodaro
Last updated on
Workspaces and organizations
Act in an organization's workspace from the Nodaro API with X-Nodaro-Workspace, bind tokens to a workspace, and manage members, invitations, sharing and usage.
Errors
Every Nodaro API error has an HTTP status and a stable code in one envelope. Learn what each code means, which errors to retry, and how failed jobs report why.