# HedaAI Product Images API

Generate 12 professional e-commerce images (8 main images + 4 A+ banners) plus complete listing copy from product photos — programmatically.

- **Base URL:** `https://hedaai.com/api/v1`
- **Price:** $1.00 per product, charged **only when generation succeeds**. Failed or partially failed tasks are never charged.
- **Model:** asynchronous. Create a task, poll its status (or receive a webhook), then fetch the finished product. A task typically takes ~10 minutes.
- **Storage:** results are saved permanently in your HedaAI account — no expiring URLs, no forced re-download window.
- **Docs for agents:** this page is mirrored at [`/developers/api.md`](/developers/api.md) (plain markdown) and indexed at [`/developers/llms.txt`](/developers/llms.txt).

## Authentication

Create an API key at [hedaai.com/settings/api-keys](https://hedaai.com/settings/api-keys) (any account; new accounts get a $2.00 signup bonus ≈ 2 free tasks). The full key (`hda_live_…`) and its webhook secret (`whsec_…`) are shown **once** at creation — store them securely. Keys can be named, capped, and revoked at any time.

Send the key as a Bearer token on every request:

```
Authorization: Bearer hda_live_...
```

Keys are stored hashed on our side and are bound to your account only — an API key can never read another account's data.

**Sandbox mode (free testing):** until your account has made a real payment (any top-up or unlock), generated images carry a watermark and no 2K upscale — everything else behaves identically. `GET /v1/account` reports `"mode": "sandbox_watermarked"`. Your first real payment automatically removes watermarks from all previously generated products and enables 2K.

**Spend caps:** each key has an optional `spend_cap_cents`. Default is unlimited. When the cap is reached, task creation fails with `spend_cap_exceeded` (403).

## Response envelope

Every response is wrapped in:

```json
{ "status": 200, "code": 200, "message": "", "data": { } }
```

Errors carry an `error_code` for programmatic matching:

```json
{ "status": 402, "code": 402, "message": "Not enough balance. Top up to continue.", "error_code": "insufficient_credits", "data": { "balance_cents": 37 } }
```

**Transport errors vs task failures:** HTTP error statuses mean the *request* failed (bad params, auth, limits). A task that fails during generation is **not** an HTTP error — polling always returns 200 with `status: "failed"` and an `error` field on the task object, and you are not charged.

## Quickstart

```bash
# 1. Create a task
curl -X POST https://hedaai.com/api/v1/tasks \
  -H "Authorization: Bearer $HEDAAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-12345-attempt-1" \
  -d '{
    "product_name": "stainless steel insulated water bottle",
    "images": ["https://your-cdn.example.com/bottle-front.jpg", "https://your-cdn.example.com/bottle-side.jpg"],
    "aspect_ratio": "1:1",
    "category": "insulated water bottle 750ml",
    "target_market": "amazon_us"
  }'
# → { "data": { "task_id": "3f8a…", "status": "queued", "urls": { "get": "/api/v1/tasks/3f8a…" } } }

# 2. Poll until terminal (every 15–30s)
curl -H "Authorization: Bearer $HEDAAI_API_KEY" https://hedaai.com/api/v1/tasks/3f8a…
# → { "data": { "status": "succeeded", "product_id": "9c1b…", "cost_cents": 100 } }

# 3. Fetch the finished product
curl -H "Authorization: Bearer $HEDAAI_API_KEY" https://hedaai.com/api/v1/products/9c1b…
```

## Endpoints

### POST /v1/tasks — create a generation task

Headers: optional `Idempotency-Key` (≤64 chars, see [Idempotency](#idempotency)).

| Field | Type | Required | Notes |
|---|---|---|---|
| `product_name` | string | **yes** | Short product name. Strong grounding signal — be specific. |
| `images` | string[1–8] | **yes** | Product photo URLs (http/https, publicly reachable, ≤15MB each) and/or base64 / data-URIs. Multi-angle photos improve fidelity. |
| `aspect_ratio` | string | **yes** | Main-image ratio: `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `9:16`, `16:9`. Amazon main images use `1:1`. |
| `category` | string | no | Concrete category (e.g. "vacuum cleaner replacement filter"). Recommended. |
| `brand_name` | string | no | Rendered into brand-story imagery and listing copy. |
| `key_features` | string[≤10] | no | Must-show selling points. |
| `specs` | object | no | `{material, colorway, key_components[], usage_context, use_scenarios[], dimensions[], scale}`. `dimensions` are "Label: value" lines (e.g. `"Height: 24 cm"`). Recommended — the API has no interactive review step, so grounding comes from what you send. |
| `target_market` | string | no | `amazon_us` (default), `shopify`, `ebay`. |
| `copy_language` | string | no | Listing copy locale (`en-US`, `de-DE`, `ja-JP`, …). Default derives from `target_market`. |
| `aplus_format` | string | no | A+ banner format: `banner` (16:9, default), `aplus_3_4`, `aplus_9_16`, `standard_aplus` / `premium` (Amazon only). |
| `no_human_models` | bool | no | `true` = no people in any image. |
| `supplementary_text` | string ≤5000 | no | Free-text specs/manual excerpts for the AI to read. |
| `callback_url` | string | no | Webhook for terminal status (see [Webhooks](#webhooks)). |

Returns a **task object**:

```json
{
  "task_id": "3f8a…", "status": "queued", "progress": 0,
  "queue_position": 2, "eta_seconds": 840, "cost_cents": 0,
  "created_at": "2026-07-17T08:00:00Z",
  "urls": { "get": "/api/v1/tasks/3f8a…", "cancel": "/api/v1/tasks/3f8a…/cancel" }
}
```

### GET /v1/tasks/{task_id} — poll status

Task `status` lifecycle: `queued` → `processing` → `succeeded` | `partial_failed` | `failed` | `canceled`.

- `succeeded` — all 12 images done; `product_id` set; `cost_cents: 100` charged.
- `partial_failed` — some images failed; the product with successful images is still saved (`product_id` set); **not charged**.
- `failed` / `canceled` — **not charged**. `error` explains the failure.

Poll every 15–30 seconds. `queue_position`/`eta_seconds` are populated while queued.

### POST /v1/tasks/{task_id}/cancel

Cancels a task. Free while queued; a task that already completed is unaffected (idempotent).

### GET /v1/products/{product_id} — fetch the finished product

```json
{
  "product_id": "9c1b…", "name": "stainless steel insulated water bottle",
  "target_market": "amazon_us", "aspect_ratio": "1:1", "watermarked": false,
  "listing_copy": {
    "title": "…", "bullet_points": ["…", "…", "…", "…", "…"],
    "description": "…", "search_terms": "…",
    "meta_title": "…", "meta_description": "…", "meta_keywords": ["…"], "language": "en"
  },
  "images": [
    { "slice_id": "M1", "section": "main", "url": "https://images.hedaai.com/…", "url_2k": "https://images.hedaai.com/…", "width": 2048, "height": 2048, "sort_order": 1 }
  ],
  "created_at": "2026-07-17T08:11:32Z"
}
```

- `M1–M8` are the 8 main images, `A1–A4` the A+ banners.
- `url_2k` appears for paid accounts after the async 2K upscale finishes (main images only) — it may lag `succeeded` by a couple of minutes.
- Image rows appear as uploads finish; immediately after `succeeded` a row can briefly have an empty/pending URL. Re-fetch after a few seconds if you see fewer than 12 images.
- URLs are permanent (served from our CDN); you may hotlink or copy them.

### GET /v1/account — balance and key info

```json
{
  "balance_cents": 1250, "paid_balance_cents": 1000, "bonus_balance_cents": 250,
  "has_paid": true, "mode": "live", "price_per_task_cents": 100,
  "key": { "name": "prod-integration", "key_prefix": "hda_live_ab12…cd34", "spend_cap_cents": null, "spent_cents": 700 }
}
```

## Idempotency

Pass an `Idempotency-Key` header (≤64 chars) on `POST /v1/tasks`. Retrying with the **same key and same body** returns the original task instead of creating (and eventually charging) a duplicate. The same key with a **different body** returns `409 idempotency_conflict`. Recommended for all agent/automation callers — network retries are free from double-spend.

## Webhooks

Set `callback_url` on task creation to receive a `POST` when the task reaches a terminal status. Event types: `task.succeeded`, `task.partial_failed`, `task.failed`, `task.canceled`.

```json
{ "id": "evt_…", "type": "task.succeeded", "created_at": "2026-07-17T08:11:35Z", "data": { /* task object, same shape as GET /v1/tasks/{id} */ } }
```

Signatures follow the [Standard Webhooks](https://www.standardwebhooks.com/) spec. Headers: `webhook-id`, `webhook-timestamp`, `webhook-signature: v1,<base64>`. Verify with your key's `webhook_secret`:

```python
import hmac, hashlib, base64

def verify(secret: str, msg_id: str, timestamp: str, body: bytes, signature_header: str) -> bool:
    key = base64.b64decode(secret.removeprefix("whsec_"))
    expected = base64.b64encode(hmac.new(key, f"{msg_id}.{timestamp}.".encode() + body, hashlib.sha256).digest()).decode()
    return any(hmac.compare_digest(expected, s.split(",", 1)[1]) for s in signature_header.split() if s.startswith("v1,"))
```

Respond with any 2xx quickly (do heavy work async). Delivery is attempted once immediately, then retried after **3s, 6s and 12s**; after that the webhook is marked failed (task state is still available by polling). Reject stale timestamps (>5 min) and de-duplicate by `id` for safety. `callback_url` must be a public http(s) endpoint — internal/private addresses are rejected.

## Rate limits & concurrency

| Limit | Value | On exceed |
|---|---|---|
| Requests per key | 120/min (all endpoints) | `429 rate_limited`; check `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` response headers |
| In-flight tasks per key | 2 (queued + processing) | `429 concurrency_limit` — wait for a task to finish |
| Platform-wide API capacity | dynamic | `429 api_capacity` — retry in a few minutes |

Generation is heavy (13–20 AI image calls per task), so throughput is governed by in-flight task slots rather than request rate. Need more concurrency? Contact us.

## Errors

| `error_code` | HTTP | Meaning |
|---|---|---|
| `bad_request` | 400 | Invalid parameters. The message pinpoints the field (e.g. `images[2]: image download failed with HTTP 403`). |
| `unauthorized` | 401 | Missing/invalid API key (or the key was revoked). |
| `account_disabled` | 403 | Account banned — keys stop working. |
| `email_not_verified` / `email_bounced` | 403 | Account email must be verified/deliverable to spend. |
| `insufficient_credits` | 402 | Balance too low; `data.balance_cents` included. Top up at hedaai.com. |
| `spend_cap_exceeded` | 403 | This key's spend cap is exhausted. |
| `idempotency_conflict` | 409 | Same `Idempotency-Key`, different body. |
| `concurrency_limit` | 429 | Too many in-flight tasks on this key. |
| `api_capacity` | 429 | Platform API capacity saturated; retry later. |
| `rate_limited` | 429 | Per-key request rate exceeded. |
| `not_found` | 404 | Task/product doesn't exist **or belongs to another account**. |
| `generation_failed` / `internal_error` | 500 | Server-side failure; safe to retry (use idempotency keys). |

## Code examples

### Python

```python
import time, requests

API = "https://hedaai.com/api/v1"
H = {"Authorization": "Bearer hda_live_..."}

task = requests.post(f"{API}/tasks", headers={**H, "Idempotency-Key": "sku-8842-v1"}, json={
    "product_name": "stainless steel insulated water bottle",
    "images": ["https://your-cdn.example.com/bottle.jpg"],
    "aspect_ratio": "1:1",
}).json()["data"]

while task["status"] in ("queued", "processing"):
    time.sleep(20)
    task = requests.get(f"{API}/tasks/{task['task_id']}", headers=H).json()["data"]

if task["status"] == "succeeded":
    product = requests.get(f"{API}/products/{task['product_id']}", headers=H).json()["data"]
    print([img["url"] for img in product["images"]])
    print(product["listing_copy"]["title"])
else:
    print("not charged:", task["status"], task.get("error"))
```

### Node.js

```javascript
const API = "https://hedaai.com/api/v1";
const H = { Authorization: "Bearer hda_live_...", "Content-Type": "application/json" };

let { data: task } = await (await fetch(`${API}/tasks`, {
  method: "POST",
  headers: { ...H, "Idempotency-Key": "sku-8842-v1" },
  body: JSON.stringify({
    product_name: "stainless steel insulated water bottle",
    images: ["https://your-cdn.example.com/bottle.jpg"],
    aspect_ratio: "1:1",
  }),
})).json();

while (["queued", "processing"].includes(task.status)) {
  await new Promise(r => setTimeout(r, 20000));
  ({ data: task } = await (await fetch(`${API}/tasks/${task.task_id}`, { headers: H })).json());
}

if (task.status === "succeeded") {
  const { data: product } = await (await fetch(`${API}/products/${task.product_id}`, { headers: H })).json();
  console.log(product.images.map(i => i.url), product.listing_copy.title);
}
```

### PHP

```php
<?php
$api = "https://hedaai.com/api/v1";
$headers = ["Authorization: Bearer hda_live_...", "Content-Type: application/json"];

function call($method, $url, $headers, $body = null) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers]);
    if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    $res = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return $res["data"];
}

$task = call("POST", "$api/tasks", array_merge($headers, ["Idempotency-Key: sku-8842-v1"]), [
    "product_name" => "stainless steel insulated water bottle",
    "images" => ["https://your-cdn.example.com/bottle.jpg"],
    "aspect_ratio" => "1:1",
]);

while (in_array($task["status"], ["queued", "processing"])) {
    sleep(20);
    $task = call("GET", "$api/tasks/{$task['task_id']}", $headers);
}

if ($task["status"] === "succeeded") {
    $product = call("GET", "$api/products/{$task['product_id']}", $headers);
    foreach ($product["images"] as $img) echo $img["url"], "\n";
}
```

### Java

```java
import java.net.URI;
import java.net.http.*;

var client = HttpClient.newHttpClient();
var api = "https://hedaai.com/api/v1";
var key = "Bearer hda_live_...";

var create = HttpRequest.newBuilder(URI.create(api + "/tasks"))
    .header("Authorization", key)
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", "sku-8842-v1")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {"product_name":"stainless steel insulated water bottle",
         "images":["https://your-cdn.example.com/bottle.jpg"],
         "aspect_ratio":"1:1"}"""))
    .build();
System.out.println(client.send(create, HttpResponse.BodyHandlers.ofString()).body());
// Parse data.task_id with your JSON library, then GET /tasks/{id} every 20s until terminal.
```

### Go

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

const api = "https://hedaai.com/api/v1"

type envelope struct {
	Data map[string]any `json:"data"`
}

func call(method, url string, body []byte) map[string]any {
	req, _ := http.NewRequest(method, url, bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer hda_live_...")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	var env envelope
	json.NewDecoder(resp.Body).Decode(&env)
	return env.Data
}

func main() {
	payload, _ := json.Marshal(map[string]any{
		"product_name": "stainless steel insulated water bottle",
		"images":       []string{"https://your-cdn.example.com/bottle.jpg"},
		"aspect_ratio": "1:1",
	})
	task := call("POST", api+"/tasks", payload)
	for task["status"] == "queued" || task["status"] == "processing" {
		time.Sleep(20 * time.Second)
		task = call("GET", fmt.Sprintf("%s/tasks/%s", api, task["task_id"]), nil)
	}
	fmt.Println(task["status"], task["product_id"])
}
```

## MCP server (for AI agents)

HedaAI ships a hosted [Model Context Protocol](https://modelcontextprotocol.io/) server so agents can generate product images directly:

- **Endpoint:** `https://hedaai.com/api/v1/mcp` (Streamable HTTP, stateless)
- **Auth:** the same `Authorization: Bearer hda_live_…` header
- **Tools:** `create_product_shoot`, `get_task`, `get_product`, `get_balance`

Claude Code:

```bash
claude mcp add --transport http hedaai https://hedaai.com/api/v1/mcp \
  --header "Authorization: Bearer $HEDAAI_API_KEY"
```

Generic JSON config (Cursor and most MCP clients):

```json
{
  "mcpServers": {
    "hedaai": {
      "url": "https://hedaai.com/api/v1/mcp",
      "headers": { "Authorization": "Bearer hda_live_..." }
    }
  }
}
```

Generation is asynchronous: agents call `create_product_shoot` (optionally with an `idempotency_key`), poll `get_task` every 30–60s until terminal, then call `get_product` for image URLs and listing copy. Clients that require OAuth (e.g. claude.ai custom connectors) are not supported yet — use API-key-capable clients such as Claude Code, Cursor, or agent SDKs.

## Pricing

| Item | Price |
|---|---|
| Product shoot (12 images + listing copy) | **$1.00** — charged on success only |
| Signup bonus | $2.00 (≈ 2 free tasks, watermarked until first payment) |
| Balance top-up | $10–$1000 at [hedaai.com/payment/packages](https://hedaai.com/payment/packages); ≥$50 +5% bonus, ≥$100 +10% |

Balance never expires. No subscription. Current price is always available at `GET /v1/account` (`price_per_task_cents`).

---

Questions? [Contact us](https://hedaai.com/contact) — or open the interactive app at [hedaai.com](https://hedaai.com) to try generation in the browser first.
