Tracking Job Costs
The /v2/job and /v2/job/async endpoints accept an optional ?price=true query parameter that returns the dollar cost of each completed job alongside the result. This lets you pass through the exact cost to your end users, log spend to a database, or enforce per-token budgets without maintaining your own copy of the price sheet.
This guide shows the full pattern for a single job and a small loop that accumulates total spend across multiple generations.

Project Setup
Section titled “Project Setup”# Create a project directory.mkdir prodia-tracking-costscd prodia-tracking-costsInstall Node (if not already installed):
brew install node# Close the current terminal and open a new one so that node is available.apt install node# Close the current terminal and open a new one so that node is available.winget install -e --id OpenJS.NodeJS.LTS# Close the current terminal and open a new one so that node is available.Create project skeleton:
# Requires node --version >= 18# Initialize the project with npm.npm init -y
# Install the prodia-js library.npm install prodia --saveInstall Python (if not already installed):
brew install python# Close the current terminal and open a new one so that python is available.apt install python3 python3-venv python-is-python3# Close the current terminal and open a new one so that python is available.winget install -e --id Python.Python.3.12# Close the current terminal and open a new one so that python is available.# Requires python --version >= 3.12python -m venv venvsource venv/bin/activatepip install requestsInstall curl (if not already installed):
brew install curl# Close the current terminal and open a new one so that curl is available.apt install curl# Close the current terminal and open a new one so that curl is available.# NOTE: Windows 10 and up have curl installed by default and this can be# skipped.winget install -e --id cURL.cURL# Close the current terminal and open a new one so that curl is available.# Export your token so it can be used by the main code.export PRODIA_TOKEN=your-token-hereYour token is exported to an environment variable. If you close or switch your
shell you’ll need to run export PRODIA_TOKEN=your-token-here again.
Create a main file for your project:
const { createProdia } = require("prodia/v2");
const prodia = createProdia({ token: process.env.PRODIA_TOKEN // get it from environment});Create the following main.py
from requests.adapters import HTTPAdapter, Retryimport osimport requestsimport sys
prodia_token = os.getenv('PRODIA_TOKEN')prodia_url = 'https://inference.prodia.com/v2/job'
session = requests.Session()retries = Retry(allowed_methods=None, status_forcelist=Retry.RETRY_AFTER_STATUS_CODES)session.mount('http://', HTTPAdapter(max_retries=retries))session.mount('https://', HTTPAdapter(max_retries=retries))session.headers.update({'Authorization': f"Bearer {prodia_token}"})set -euo pipefailYou’re now ready to make some API calls!
Get the price for a single job
Section titled “Get the price for a single job”Add ?price=true to the request URL. The job result will include a price object with the billing product code and the dollars cost. The prodia-js SDK doesn’t expose query parameters on prodia.job(...) directly, so we use fetch to read the multipart response and pull out both the price field and the image.
const fs = require("node:fs/promises");
(async () => { const res = await fetch( "https://inference.prodia.com/v2/job?price=true", { method: "POST", headers: { Authorization: `Bearer ${process.env.PRODIA_TOKEN}`, "Content-Type": "application/json", Accept: "multipart/form-data", }, body: JSON.stringify({ type: "inference.flux-fast.schnell.txt2img.v2", config: { prompt: "a single red apple on a white background, 4k photo", seed: 42, }, }), }, );
if (!res.ok) { console.error(`Status: ${res.status}`); console.error(await res.text()); process.exit(1); }
const formData = await res.formData(); const job = JSON.parse(await formData.get("job").text()); const output = formData.get("output");
console.log(`product: ${job.price.product}`); console.log(`dollars: ${job.price.dollars}`);
await fs.writeFile( "apple.jpg", new Uint8Array(await output.arrayBuffer()), );})();node main.jsfrom requests.adapters import HTTPAdapter, Retryfrom requests_toolbelt.multipart import decoderimport jsonimport osimport requestsimport sys
prodia_token = os.getenv('PRODIA_TOKEN')prodia_url = 'https://inference.prodia.com/v2/job?price=true'
session = requests.Session()retries = Retry(allowed_methods=None, status_forcelist=Retry.RETRY_AFTER_STATUS_CODES)session.mount('http://', HTTPAdapter(max_retries=retries))session.mount('https://', HTTPAdapter(max_retries=retries))session.headers.update({'Authorization': f"Bearer {prodia_token}"})
headers = { 'Accept': 'multipart/form-data',}
job = { 'type': 'inference.flux-fast.schnell.txt2img.v2', 'config': { 'prompt': 'a single red apple on a white background, 4k photo', 'seed': 42, },}
res = session.post(prodia_url, headers=headers, json=job)print(f"Status: {res.status_code}")
if res.status_code != 200: print(res.text) sys.exit(1)
# Walk the multipart parts to find the JSON job result and the image output.parts = decoder.MultipartDecoder.from_response(res).parts
for part in parts: disposition = part.headers.get(b'Content-Disposition', b'').decode() if 'name="job"' in disposition: result = json.loads(part.content) print(f"product: {result['price']['product']}") print(f"dollars: {result['price']['dollars']}") elif 'name="output"' in disposition: with open('apple.jpg', 'wb') as f: f.write(part.content)pip install requests requests-toolbeltpython main.pyset -euo pipefail
# Capture the multipart response in a tempfile.tmp=$(mktemp)trap 'rm -f "$tmp"' EXIT
curl -sSf \ -H "Authorization: Bearer $PRODIA_TOKEN" \ -H 'Accept: multipart/form-data' \ --json '{ "type": "inference.flux-fast.schnell.txt2img.v2", "config": { "prompt": "a single red apple on a white background, 4k photo", "seed": 42 } }' \ --output "$tmp" \ --retry 3 \ 'https://inference.prodia.com/v2/job?price=true'
# Pull out the JSON job result and the JPEG image with awk.awk -v RS='\r\n--[a-f0-9]{60}' ' /name="job"/ { sub(/^[^{]*/, ""); print > "job.json" } /name="output"/ { sub(/^[^\xff]*/, ""); sub(/\r\n$/, ""); printf "%s", $0 > "apple.jpg" }' "$tmp"
# Print the price.python3 -c "import json; p = json.load(open('job.json'))['price']; print(f\"product: {p['product']}\"); print(f\"dollars: {p['dollars']}\")"bash main.shYou’ll see output like:
product: inference-flux-schnell-large-steps-4dollars: 0.0025The product is the billing line item — it’s how this job will appear on your invoice and in usage exports. The dollars value is the exact amount charged for this single call.
Accumulate cost across multiple jobs
Section titled “Accumulate cost across multiple jobs”The dollars value is per-job, so totalling spend is just a sum. This snippet generates several thumbnails in a loop and prints the running total — useful when you’re batching for a customer and want to charge them at the end.
const fs = require("node:fs/promises");
const prompts = [ "a single red apple on a white background, 4k photo", "a single yellow lemon on a white background, 4k photo", "a single green pear on a white background, 4k photo",];
(async () => { let total = 0;
for (const [i, prompt] of prompts.entries()) { const res = await fetch( "https://inference.prodia.com/v2/job?price=true", { method: "POST", headers: { Authorization: `Bearer ${process.env.PRODIA_TOKEN}`, "Content-Type": "application/json", Accept: "multipart/form-data", }, body: JSON.stringify({ type: "inference.flux-fast.schnell.txt2img.v2", config: { prompt, seed: 42 }, }), }, );
if (!res.ok) { console.error(`Job ${i} failed: ${res.status}`); continue; }
const formData = await res.formData(); const job = JSON.parse(await formData.get("job").text()); const output = formData.get("output");
total += job.price.dollars; console.log(`#${i} ${job.price.product} $${job.price.dollars.toFixed(4)}`);
await fs.writeFile( `out-${i}.jpg`, new Uint8Array(await output.arrayBuffer()), ); }
console.log(`---`); console.log(`Total: $${total.toFixed(4)}`);})();node batch.jsfrom requests.adapters import HTTPAdapter, Retryfrom requests_toolbelt.multipart import decoderimport jsonimport osimport requests
prodia_token = os.getenv('PRODIA_TOKEN')prodia_url = 'https://inference.prodia.com/v2/job?price=true'
session = requests.Session()retries = Retry(allowed_methods=None, status_forcelist=Retry.RETRY_AFTER_STATUS_CODES)session.mount('https://', HTTPAdapter(max_retries=retries))session.headers.update({'Authorization': f"Bearer {prodia_token}"})
prompts = [ 'a single red apple on a white background, 4k photo', 'a single yellow lemon on a white background, 4k photo', 'a single green pear on a white background, 4k photo',]
total = 0.0
for i, prompt in enumerate(prompts): res = session.post(prodia_url, headers={'Accept': 'multipart/form-data'}, json={ 'type': 'inference.flux-fast.schnell.txt2img.v2', 'config': {'prompt': prompt, 'seed': 42}, }) if res.status_code != 200: print(f"Job {i} failed: {res.status_code}") continue
parts = decoder.MultipartDecoder.from_response(res).parts for part in parts: disposition = part.headers.get(b'Content-Disposition', b'').decode() if 'name="job"' in disposition: result = json.loads(part.content) total += result['price']['dollars'] print(f"#{i} {result['price']['product']} ${result['price']['dollars']:.4f}") elif 'name="output"' in disposition: with open(f'out-{i}.jpg', 'wb') as f: f.write(part.content)
print('---')print(f"Total: ${total:.4f}")python batch.pyset -euo pipefail
prompts=( "a single red apple on a white background, 4k photo" "a single yellow lemon on a white background, 4k photo" "a single green pear on a white background, 4k photo")
total=0
for i in "${!prompts[@]}"; do tmp=$(mktemp) curl -sSf \ -H "Authorization: Bearer $PRODIA_TOKEN" \ -H 'Accept: multipart/form-data' \ --json "{\"type\":\"inference.flux-fast.schnell.txt2img.v2\",\"config\":{\"prompt\":\"${prompts[$i]}\",\"seed\":42}}" \ --output "$tmp" \ --retry 3 \ 'https://inference.prodia.com/v2/job?price=true'
awk -v out="out-$i.jpg" -v RS='\r\n--[a-f0-9]{60}' ' /name="job"/ { sub(/^[^{]*/, ""); print > "job.json" } /name="output"/ { sub(/^[^\xff]*/, ""); sub(/\r\n$/, ""); printf "%s", $0 > out } ' "$tmp"
dollars=$(python3 -c "import json; print(json.load(open('job.json'))['price']['dollars'])") product=$(python3 -c "import json; print(json.load(open('job.json'))['price']['product'])") echo "#$i $product \$$dollars"
total=$(python3 -c "print($total + $dollars)") rm -f "$tmp"done
echo "---"echo "Total: \$$total"bash batch.shA typical run prints something like:
#0 inference-flux-schnell-large-steps-4 $0.0025#1 inference-flux-schnell-large-steps-4 $0.0025#2 inference-flux-schnell-large-steps-4 $0.0025---Total: $0.0075- The
pricefield is only present when?price=trueis set and the job completes successfully. Failed jobs do not return a price. - Different
configvalues can resolve to differentproductcodes — for example, FLUX with 4 steps versus 28 steps, or a 720p Wan video versus 1080p — so always readdollarsfrom the response rather than hard-coding it client-side. - For a deeper reference on the response shape, see the Job Pricing reference.