# AI Image Generation and Editing API for Product Photos

> Generate a product photo with an image API, then edit it from a reference image (white background, price tag). Node and Python code, costs and errors measured.

- Published: 2026-10-18
- Updated: 2026-11-02
- Author: Gael — Tchavi founder
- Canonical: https://tchavi.com/en/blog/ai-image-generation-editing-api-product-photos

---


For a shop on Jumia, Kilimall or Instagram, two calls are enough: one generates the product photo from a text, the other edits it from that photo (white background, price tag). On my runs, a generation cost 11 credits and an edit between 17 and 96 credits, in 8 to 9 seconds.

I made the three calls for real, in October 2026, through Tchavi, with an imaginary wax-fabric handbag. I wrote the prompts in French, so the prompt strings in the code stay in French, and the measured results are exactly as I got them. Here is the code in Node.js and Python, what it cost, and what each model did well or badly.

## In short

- **Tchavi, an OpenAI-compatible AI API gateway you pay for with mobile money, generates and edits images with the same key as text.** The route is `POST /v1/images/generations`, and `POST /v1/images/edits` for the GPT Image models.
- **Measured generation**: `gpt-image-1-mini` at `low` quality, 1024x1024, cost 11 credits (about KES 0.83 at October 2026 rates) in 8.5 seconds.
- **Measured edit**: 17 credits with `gpt-image-1-mini` (background replaced, but the price text was wrong), 96 credits with `nano-banana-2` at 0.5K (background replaced, bag faithful, price readable).
- **Reference images are sent as base64** in the `images` field. GPT Image accepts 16, Nano Banana 14.
- **A GPT Image edit goes through `/images/edits`.** With `gpt-image-1-mini`, the same request sent to `/images/generations` returned a `502` and debited nothing.

## Which model for what

Not every model takes a reference image. On the ones I looked at:

| Model              | Generation | Reference image | Price settings                         |
| ------------------ | ---------- | --------------- | -------------------------------------- |
| `gpt-image-1-mini` | yes        | yes (up to 16)  | `quality`: `low`, `medium`, `high`     |
| `nano-banana-2`    | yes        | yes (up to 14)  | `resolution`: `0.5K`, `1K`, `2K`, `4K` |
| `flux-2-pro`       | yes        | no              | one price per image                    |

For `flux-2-pro`, I sent a reference image and the API answered `400 bad_request`: image input is reserved for certain model families. The [image documentation](https://tchavi.com/en/docs/image-generation) lists the families, and each model's API tab gives its exact parameters.

Credit prices for a square image, from the catalogue, with KES at October 2026 rates (about 13,204 credits per KES 1,000, so 1 credit is about KES 0.0757):

| Model              | Setting  | Credits | In KES          |
| ------------------ | -------- | ------- | --------------- |
| `gpt-image-1-mini` | `low`    | 11      | about KES 0.83  |
| `gpt-image-1-mini` | `medium` | 24      | about KES 1.82  |
| `gpt-image-1-mini` | `high`   | 77      | about KES 5.83  |
| `nano-banana-2`    | `0.5K`   | 101.28  | about KES 7.67  |
| `nano-banana-2`    | `1K`     | 142     | about KES 10.75 |
| `nano-banana-2`    | `2K`     | 214     | about KES 16.20 |

Model pages: [`gpt-image-1-mini`](https://tchavi.com/en/m/openai/gpt-image-1-mini) and [`nano-banana-2`](https://tchavi.com/en/m/google/nano-banana-2). My advice: start at the lowest-priced setting, look at the result, and only go up if the image doesn't hold up.

The image format changes the price too. A non-square `size` is billed 1.5 times the square price on `gpt-image-1-mini`: I measured 16.5 credits for a 1024x1536 at `low`, against 11 for square. For a vertical Instagram visual, that is the price to plan for.

## Step 1: generate the product photo (Node.js)

```javascript
import { writeFileSync } from 'node:fs';

const BASE = 'https://tchavi.com/api/v1';
const headers = {
  'Content-Type': 'application/json',
  Authorization: `Bearer ${process.env.TCHAVI_API_KEY}`,
};

const res = await fetch(`${BASE}/images/generations`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    model: 'gpt-image-1-mini',
    quality: 'low',
    size: '1024x1024',
    prompt:
      'Photo produit pour une boutique en ligne : un sac à main en tissu wax coloré (motifs jaune, bleu et rouge), posé sur une table en bois, lumière douce de fenêtre, fond de salon flou, rendu réaliste',
  }),
});
const json = await res.json();
if (!res.ok) throw new Error(`${json.error}: ${json.message}`);

const image = json.data[0];
const ext = image.media_type.split('/')[1]; // png, jpeg or webp
writeFileSync(`bag.${ext}`, Buffer.from(image.b64_json, 'base64'));
console.log('credits:', res.headers.get('x-credits-used'));
```

The image is in `data[0].b64_json`, its type in `data[0].media_type`. Read the type instead of assuming PNG: depending on the model, you can receive PNG, JPEG or WebP.

Result of my call: status 200, 8.5 seconds, 11 credits, a 1.5 MB PNG. The bag is legible, with plausible wax patterns, but the material looks more like quilted fabric than wax. That is the level of detail of a `low` setting.

## Step 2: edit from a reference image

This is the useful part for a seller: keep the product and change the setting. The request is the same, with an `images` field that holds your reference images in base64.

### With GPT Image: the `/images/edits` route

```javascript
import { readFileSync, writeFileSync } from 'node:fs';

const reference = readFileSync('bag.png').toString('base64');

const res = await fetch(`${BASE}/images/edits`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    model: 'gpt-image-1-mini',
    quality: 'low',
    size: '1024x1024',
    images: [reference],
    prompt:
      'Garde exactement le même sac. Remplace le fond par un fond blanc uni de studio, et ajoute une étiquette de prix ronde jaune avec le texte « 12 500 F » en haut à droite.',
  }),
});
const json = await res.json();
if (!res.ok) throw new Error(`${json.error}: ${json.message}`);
writeFileSync('bag-edited.png', Buffer.from(json.data[0].b64_json, 'base64'));
```

Measured: 200, 8.9 seconds, 17 credits (about KES 1.29). That is the 11 credits of the generation, plus 6 credits for the reference image: on `gpt-image-1-mini`, each reference image is billed on top of the image price.

I only tested `gpt-image-1-mini`, so I say nothing about the other GPT Image models, or about `/images/edits` with Nano Banana.

The background did turn white and the yellow tag is there. Two flaws: the bag's pattern was redrawn (it is no longer exactly the same bag), and the tag reads "12300F" instead of "12 500 F". Exact text in an image is the weak point of low settings. For a price tag, I advise you to add it yourself in code (an SVG image or a canvas) rather than ask the model.

### With Nano Banana 2: the `/images/generations` route

For Nano Banana, editing goes through the generation route, with `resolution` instead of `quality`:

```python
import base64
import os

import requests

BASE = "https://tchavi.com/api/v1"
reference = base64.b64encode(open("bag.png", "rb").read()).decode()

r = requests.post(
    f"{BASE}/images/generations",
    headers={"Authorization": f"Bearer {os.environ['TCHAVI_API_KEY']}"},
    json={
        "model": "nano-banana-2",
        "resolution": "0.5K",
        "images": [reference],
        "prompt": "Garde exactement le même sac. Remplace le fond par un fond blanc uni de studio, "
                  "et ajoute une étiquette de prix ronde jaune avec le texte « 12 500 F » en haut à droite.",
    },
    timeout=120,
)
data = r.json()
if not r.ok:
    raise RuntimeError(f"{data['error']}: {data['message']}")

image = data["data"][0]
ext = image["media_type"].split("/")[1]
open(f"bag-nano.{ext}", "wb").write(base64.b64decode(image["b64_json"]))
print("credits:", r.headers.get("x-credits-used"))
```

Measured: 200, 8.3 seconds, 96 credits (about KES 7.27), which was the catalogue price at `0.5K` that day (101.28 credits at the current rate), with no visible extra for the reference image, and an image 512 pixels on a side (that is what `0.5K` means). Here the result is clearly better: the bag is faithful to the original, the background is white, and the tag does say "12 500 F".

What this tells me, from a single run per model: at these settings, the 96-credit edit holds the product and the text better than the 17-credit one. It costs almost six times more. It is up to you to decide whether your catalogue justifies the difference. One run is not a statistic: repeat the test with your own photos before choosing.

## The settings that matter

- **`n`**: the number of images, from 1 to 4. Nano Banana models return one image per call, `n` is brought down to 1.
- **`size`** for GPT Image: `1024x1024`, `1024x1536` or `1536x1024`. Any other value returns `400`.
- **`quality`** for GPT Image: `low`, `medium`, `high`. If you forget it, `high` is billed, so the most expensive: remember to set it.
- **`resolution`** for Nano Banana: `0.5K`, `1K`, `2K`, `4K`, and `aspect_ratio` for framing (for example `1:1`, `4:3`, `9:16`).
- **`response_format`**: `b64_json` by default, or `url` depending on the model.

## Common errors and what they mean

I triggered these errors on purpose. None of them debited credits.

| What I sent                                               | Response              | What it means                                                  |
| --------------------------------------------------------- | --------------------- | -------------------------------------------------------------- |
| `size: "512x512"` on `gpt-image-1-mini`                   | `400 bad_request`     | The message lists the accepted sizes                           |
| `quality: "ultra"`                                        | `400 bad_request`     | The message lists `low`, `medium`, `high`                      |
| `resolution: "8K"` on `nano-banana-2`                     | `400 bad_request`     | The message lists `0.5K`, `1K`, `2K`, `4K`                     |
| `aspect_ratio: "5:1"` on `nano-banana-2`                  | `400 bad_request`     | The message lists the accepted ratios                          |
| `images` on `flux-2-pro`                                  | `400 bad_request`     | This model does not accept a reference image                   |
| `image` and `images` together                             | `400 bad_request`     | Send one or the other                                          |
| A model that does not exist                               | `404 model_not_found` | Check the identifier with `GET /v1/models`                     |
| `images` on `/images/generations` with `gpt-image-1-mini` | `502 provider_error`  | Use `/images/edits` for GPT Image models                       |
| Body without `model` or without `prompt`                  | `502 provider_error`  | Validate your request in code: the API does not say it clearly |

The other codes come from the [errors page](https://tchavi.com/en/docs/errors): `401 invalid_api_key` (wrong key), `402 insufficient_credits` (balance too low, with `credits_remaining` and `credits_required` in `details`), `429 rate_limit_exceeded` (wait `Retry-After` seconds). If you get a `429`, a retry loop with a pause fixes it.

A `502` does not always mean "outage": in these last two cases, it was a malformed request. Before you retry in a loop, re-read your request body.

## A ready-made loop for a catalogue

If you have twenty products to put on a white background, call the route in series with a pause on `429`, and keep the total cost:

```javascript
async function editProduct(file) {
  const images = [readFileSync(file).toString('base64')];
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(`${BASE}/images/edits`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        model: 'gpt-image-1-mini',
        quality: 'low',
        images,
        prompt: 'Garde le même produit, fond blanc uni de studio.',
      }),
    });
    if (res.status === 429) {
      const { details } = await res.json();
      await new Promise((r) => setTimeout(r, (details?.retry_after ?? 2) * 1000));
      continue;
    }
    const json = await res.json();
    if (!res.ok) throw new Error(`${json.error}: ${json.message}`);
    return { image: json.data[0], credits: Number(res.headers.get('x-credits-used')) };
  }
  throw new Error('too many 429s');
}
```

At 17 credits per edit, twenty products come to 340 credits, about KES 26. That is a calculation from my measurement, not a measurement on twenty images.

## FAQ

### How much does an image generated by the API cost?

On `gpt-image-1-mini`, 11 credits at `low` quality, 24 at `medium` and 77 at `high`, for a square image. At October 2026 rates, that is about KES 0.83 for the simplest. The real cost of each call is in the `x-credits-used` header.

### Which models accept a reference image?

`gpt-image-1-mini` (up to 16 images) and `nano-banana-2` (up to 14) accept it, from my runs and the documentation. `flux-2-pro` refuses it with a `400`. For another model, look at its API tab.

### Why doesn't my edited image look like the product any more?

The model redraws the image from your reference: it does not copy it. The lower the setting, the more it invents. Say "keep exactly the same product", try a higher setting, and run several attempts before you publish.

### How do I get exact text on the image, like a price?

Don't count on it. My 17-credit run wrote "12300F" instead of "12 500 F". Add the price in code afterwards, it is free and exact.

### Which image format do I receive?

Read `data[0].media_type`: `image/png`, `image/jpeg` or `image/webp`. My three images were PNGs, from 0.4 to 1.5 MB, so compress them before you put them on your shop.

### Should I send the image as base64 or as a URL?

For the reference, I used raw base64 in `images`. The field also accepts a `data:image/png;base64,...` string.

### How do I pay without a bank card?

With mobile money: the minimum top-up is KES 250, and there is no subscription and no free credits at signup. Your first paid top-up adds 150 bonus credits. See [countries and operators](https://tchavi.com/en/docs/payments), including M-Pesa in Kenya.

[create your account and top up KES 250](https://tchavi.com/en/register?next=%2Fdashboard%2Fbilling%3Famount%3D250%26currency%3DKES%26from%3Dblog-images-api-en)

> _This article is part of the series "Building an AI product from Africa". Tchavi is an OpenAI-compatible AI API gateway: one key gives access to over 100 text, image, audio and video models, and you pay for it with mobile money, no bank card ([countries and operators](https://tchavi.com/en/docs/payments)). More guides on [the blog](https://tchavi.com/en/blog). Follow us: [LinkedIn](https://www.linkedin.com/company/tchavi) and [X](https://x.com/tchaviai)._

