# Tracking Job Costs

Source: https://docs.prodia.com/guides/tracking-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.

![A single red apple on a white background — the example image generated below](https://docs.prodia.com/llms/assets/e72747925412-red-apple.jpg)

### Project Setup

```bash
# Create a project directory.
mkdir prodia-tracking-costs
cd prodia-tracking-costs
```

### prodia-js

Install Node (if not already installed):

### macOS

```bash
brew install node
# Close the current terminal and open a new one so that node is available.
```

### Linux

```bash
apt install node
# Close the current terminal and open a new one so that node is available.
```

### Windows

```bash
winget install -e --id OpenJS.NodeJS.LTS
# Close the current terminal and open a new one so that node is available.
```

Create project skeleton:

```bash
# Requires node --version >= 18
# Initialize the project with npm.
npm init -y

# Install the prodia-js library.
npm install prodia --save
```

### requests

Install Python (if not already installed):

### macOS

```bash
brew install python
# Close the current terminal and open a new one so that python is available.
```

### Linux

```bash
apt install python3 python3-venv python-is-python3
# Close the current terminal and open a new one so that python is available.
```

### Windows

```bash
winget install -e --id Python.Python.3.12
# Close the current terminal and open a new one so that python is available.
```

```bash
# Requires python --version >= 3.12
python -m venv venv
source venv/bin/activate
pip install requests
```

### curl

Install curl (if not already installed):

### macOS

```bash
brew install curl
# Close the current terminal and open a new one so that curl is available.
```

### Linux

```bash
apt install curl
# Close the current terminal and open a new one so that curl is available.
```

### Windows

```bash
# 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.
```

```bash
# Export your token so it can be used by the main code.
export PRODIA_TOKEN=your-token-here
```

> note:
>
> Your 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:

### prodia-js

```js title="main.js"
const { createProdia } = require("prodia/v2");

const prodia = createProdia({
    token: process.env.PRODIA_TOKEN // get it from environment
});
```

### requests

Create the following `main.py`

```python title="main.py"
from requests.adapters import HTTPAdapter, Retry
import os
import requests
import 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}"})
```

### curl

```bash title="main.sh"
set -euo pipefail
```

You're now ready to make some API calls!

### 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.

### prodia-js

```javascript title="main.js"
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()),
  );
})();
```

```bash
node main.js
```

### requests

```python title="main.py"
from requests.adapters import HTTPAdapter, Retry
from requests_toolbelt.multipart import decoder
import json
import os
import requests
import 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)
```

```bash
pip install requests requests-toolbelt
python main.py
```

### curl

```bash title="main.sh"
set -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
bash main.sh
```

You'll see output like:

```
product: inference-flux-schnell-large-steps-4
dollars: 0.0025
```

The `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.

> tip:
>
> The same `?price=true` flag works on `/v2/job/async` too. The price appears on `GET /v2/job/async/:id/job.json` once the job reaches the `processed` state. See the [polling async jobs guide](https://docs.prodia.com/guides/polling-async-jobs/) for the full async flow.

### 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.

### prodia-js

```javascript title="batch.js"
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)}`);
})();
```

```bash
node batch.js
```

### requests

```python title="batch.py"
from requests.adapters import HTTPAdapter, Retry
from requests_toolbelt.multipart import decoder
import json
import os
import 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}")
```

```bash
python batch.py
```

### curl

```bash title="batch.sh"
set -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
bash batch.sh
```

A 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
```

### Notes

- The `price` field is only present when `?price=true` is set **and** the job completes successfully. Failed jobs do not return a price.
- Different `config` values can resolve to different `product` codes — for example, FLUX with 4 steps versus 28 steps, or a 720p Wan video versus 1080p — so always read `dollars` from the response rather than hard-coding it client-side.
- For a deeper reference on the response shape, see the [Job Pricing reference](https://docs.prodia.com/reference/price/).

[Polling Async Jobs](https://docs.prodia.com/guides/polling-async-jobs/) — Use ?price=true with the async API for long-running video jobs.
