ReWords.AI API

Exemples

Clients Node.js et Python complets pour l’API ReWords.AI, et chaque réponse que vous rencontrerez — OCR, une modification en file, en cours, réussie, partiellement échouée et échouée, et chaque erreur.

Un client complet

Les deux scripts envoient un fichier local, attendent la tâche selon la règle d’interrogation, respectent Retry-After, renvoient après une erreur réseau ou un 5xx avec la même Idempotency-Key (une tâche n’est donc jamais créée ni facturée deux fois), enregistrent chaque résultat réussi et affichent le texte trouvé dans l’image quand from ne correspond pas.

edit-image.mjs — Node.js 18+
import fs from 'node:fs/promises';

const BASE = 'https://api.rewords.ai/v1';
const KEY = process.env.REWORDS_API_KEY;
const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));

async function call(method, path, body, headers = {}) {
  for (let attempt = 1; ; attempt++) {
    let res;
    try {
      res = await fetch(BASE + path, {
        method,
        headers: { Authorization: `Bearer ${KEY}`, ...(body ? { 'Content-Type': 'application/json' } : {}), ...headers },
        body: body ? JSON.stringify(body) : undefined,
        signal: AbortSignal.timeout(90_000),
      });
    } catch (e) {                                            // network error or timeout: safe to resend,
      if (attempt >= 5) throw e;                             // the Idempotency-Key prevents a second job
      await sleep(2 ** attempt);
      continue;
    }
    if (res.status === 429 || res.status === 503) {          // rate_limited or queue_full: wait and retry
      await sleep(Number(res.headers.get('Retry-After') ?? 30));
      continue;
    }
    if (res.status >= 500 && attempt < 5) { await sleep(2 ** attempt); continue; }
    const data = await res.json().catch(() => ({ error: { code: 'http_' + res.status, message: res.statusText } }));
    if (!res.ok) throw Object.assign(new Error(data.error.message), { status: res.status, error: data.error });
    return data;
  }
}

async function editImage(file, replacements, idempotencyKey) {
  const image_base64 = (await fs.readFile(file)).toString('base64');
  let job = await call('POST', '/edits', { image_base64, replacements }, { 'Idempotency-Key': idempotencyKey });
  while (job.status === 'queued' || job.status === 'processing') {
    const waitingBehindOthers = job.status === 'queued' && job.queue_position > 0;
    await sleep(waitingBehindOthers ? 30 : 5);
    job = await call('GET', `/edits/${job.id}`);
  }
  if (job.status === 'failed') throw Object.assign(new Error(job.error.message), { error: job.error });
  for (const [name, r] of Object.entries(job.results)) {
    if (r.status !== 'succeeded') { console.log(`${name}: ${r.status} (${r.reason})`); continue; }
    const file = await fetch(r.url);
    if (!file.ok) throw new Error(`Downloading ${name} failed: HTTP ${file.status}`);
    await fs.writeFile(`${job.id}_${name}.jpg`, Buffer.from(await file.arrayBuffer()));
    console.log(`${name}: saved ${job.id}_${name}.jpg`);
  }
  console.log(`charged ${job.charged}, ${job.balance} edits left`);
  return job;
}

try {
  await editImage(process.argv[2], [{ from: 'SALE', to: 'HOT DEAL' }], `banner-${Date.now()}`);
} catch (e) {
  if (e.error?.code === 'text_not_found') console.error('Not found. Text in the image:', e.error.details.texts.map((t) => t.text));
  else if (e.status) console.error(`HTTP ${e.status}`, e.error.code, e.message);
  else if (e.error) console.error('job failed:', e.error.code, e.message);
  else console.error('network error:', e.message);
  process.exitCode = 1;
}
edit_image.py — Python 3.8+, pip install requests
import base64, os, sys, time
from typing import Optional
import requests

BASE = "https://api.rewords.ai/v1"
KEY = os.environ["REWORDS_API_KEY"]


class ReWordsError(Exception):
    """status is the HTTP status for a rejected request, or None for a job that failed while processing."""

    def __init__(self, status: Optional[int], error: dict):
        super().__init__(error["message"])
        self.status, self.code, self.details = status, error["code"], error.get("details", {})


def call(method, path, body=None, headers=None):
    attempt = 0
    while True:
        attempt += 1
        try:
            res = requests.request(method, BASE + path, json=body, timeout=90,
                                   headers={"Authorization": f"Bearer {KEY}", **(headers or {})})
        except requests.RequestException:  # network error or timeout: safe to resend,
            if attempt >= 5:                 # the Idempotency-Key prevents a second job
                raise
            time.sleep(2 ** attempt)
            continue
        if res.status_code in (429, 503):  # rate_limited or queue_full: wait and retry
            time.sleep(int(res.headers.get("Retry-After", 30)))
            continue
        if res.status_code >= 500 and attempt < 5:
            time.sleep(2 ** attempt)
            continue
        try:
            data = res.json()
        except ValueError:
            data = {"error": {"code": f"http_{res.status_code}", "message": res.reason}}
        if not res.ok:
            raise ReWordsError(res.status_code, data["error"])
        return data


def edit_image(path, replacements, idempotency_key):
    with open(path, "rb") as f:
        image_base64 = base64.b64encode(f.read()).decode()
    job = call("POST", "/edits", {"image_base64": image_base64, "replacements": replacements},
               {"Idempotency-Key": idempotency_key})
    while job["status"] in ("queued", "processing"):
        waiting_behind_others = job["status"] == "queued" and job.get("queue_position", 0) > 0
        time.sleep(30 if waiting_behind_others else 5)
        job = call("GET", f"/edits/{job['id']}")
    if job["status"] == "failed":
        raise ReWordsError(None, job["error"])
    for name, r in job["results"].items():
        if r["status"] != "succeeded":
            print(f"{name}: {r['status']} ({r['reason']})")
            continue
        file = requests.get(r["url"], timeout=60)
        file.raise_for_status()
        with open(f"{job['id']}_{name}.jpg", "wb") as out:
            out.write(file.content)
        print(f"{name}: saved {job['id']}_{name}.jpg")
    print(f"charged {job['charged']}, {job['balance']} edits left")
    return job


if __name__ == "__main__":
    try:
        edit_image(sys.argv[1], [{"from": "SALE", "to": "HOT DEAL"}], f"banner-{int(time.time())}")
    except ReWordsError as e:
        if e.code == "text_not_found":
            print("Not found. Text in the image:", [t["text"] for t in e.details["texts"]])
        else:
            print(f"HTTP {e.status}" if e.status else "job failed:", e.code, e)
        sys.exit(1)
    except requests.RequestException as e:
        print("network error:", e)
        sys.exit(1)
REWORDS_API_KEY=rw_live_your_key node edit-image.mjs banner.png
REWORDS_API_KEY=rw_live_your_key python edit_image.py banner.png
A: saved edit_68805a1cefad_A.jpg
B: saved edit_68805a1cefad_B.jpg
C: saved edit_68805a1cefad_C.jpg
charged 1, 59 edits left

Chaque réponse, étape par étape

OCR — POST /ocr → 200

{
  "image_id": "img_118ba6012ee7",
  "width": 2560,
  "height": 1302,
  "texts": [
    { "id": "t1", "text": "SALE", "box": [120, 80, 300, 60] },
    { "id": "t2", "text": "50% OFF", "box": [140, 160, 260, 50] },
    { "id": "t3", "text": "Summer Collection", "box": [100, 700, 800, 80] }
  ],
  "charged_ocr": 1
}

La même image à nouveau dans les 24 heures renvoie un nouvel image_id et "charged_ocr": 0. Une image sans texte renvoie "texts": [] et est quand même facturée.

Envoyer — POST /edits → 202

curl https://api.rewords.ai/v1/edits \
  -H "Authorization: Bearer $REWORDS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"image_id": "img_118ba6012ee7",
       "replacements": [{"from": "SALE", "to": "HOT DEAL", "box": [120, 80, 300, 60]},
                        {"from": "50% OFF", "to": "70% OFF"}]}'
{
  "id": "edit_68805a1cefad",
  "status": "queued",
  "queue_position": 0,
  "estimated_seconds": 120,
  "replacements": [
    { "from": "SALE", "to": "HOT DEAL", "box": [120, 80, 300, 60] },
    { "from": "50% OFF", "to": "70% OFF", "box": [140, 160, 260, 50] }
  ],
  "balance": 59,
  "created_at": "2026-11-04T13:55:33.240Z"
}

replacements montre la boîte trouvée pour 50% OFF. balance a déjà réservé la modification de cette tâche.

Interroger — GET /edits/{id} → 200, en cours

{
  "id": "edit_68805a1cefad",
  "status": "processing",
  "replacements": [
    { "from": "SALE", "to": "HOT DEAL", "box": [120, 80, 300, 60] },
    { "from": "50% OFF", "to": "70% OFF", "box": [140, 160, 260, 50] }
  ],
  "balance": 59,
  "created_at": "2026-11-04T13:55:33.240Z"
}

Réussie

{
  "id": "edit_68805a1cefad",
  "status": "succeeded",
  "results": {
    "A": { "status": "succeeded", "url": "https://img.rewords.ai/api/edit_68805a1cefad_a.jpg" },
    "B": { "status": "succeeded", "url": "https://img.rewords.ai/api/edit_68805a1cefad_b.jpg" },
    "C": { "status": "succeeded", "url": "https://img.rewords.ai/api/edit_68805a1cefad_c.jpg" }
  },
  "replacements": [
    { "from": "SALE", "to": "HOT DEAL", "box": [120, 80, 300, 60] },
    { "from": "50% OFF", "to": "70% OFF", "box": [140, 160, 260, 50] }
  ],
  "charged": 1,
  "balance": 59,
  "created_at": "2026-11-04T13:55:33.240Z",
  "completed_at": "2026-11-04T13:55:38.350Z"
}

Réussie, mais un résultat a échoué — quand même facturée

Il suffit que l’un de A et B réussisse. Ici A a échoué, utilisez donc B :

{
  "id": "edit_9e88b77efd21",
  "status": "succeeded",
  "results": {
    "A": { "status": "failed", "reason": "ai_generation_failed" },
    "B": { "status": "succeeded", "url": "https://img.rewords.ai/api/edit_9e88b77efd21_b.jpg" },
    "C": { "status": "skipped", "reason": "aspect_ratio_unsupported" }
  },
  "replacements": [{ "from": "SALE", "to": "HOT DEAL", "box": [120, 80, 300, 60] }],
  "charged": 1,
  "balance": 58,
  "created_at": "2026-11-04T13:55:38.693Z",
  "completed_at": "2026-11-04T13:55:43.766Z"
}

Échouée — non facturée

{
  "id": "edit_369c949cd4ed",
  "status": "failed",
  "results": {
    "A": { "status": "failed", "reason": "ai_generation_failed" },
    "B": { "status": "failed", "reason": "ai_generation_failed" },
    "C": { "status": "skipped", "reason": "ai_generation_failed" }
  },
  "replacements": [{ "from": "SALE", "to": "HOT DEAL", "box": [120, 80, 300, 60] }],
  "charged": 0,
  "balance": 58,
  "error": { "code": "ai_generation_failed", "message": "The AI model could not produce a usable image.", "details": {} },
  "created_at": "2026-11-04T13:55:38.691Z",
  "completed_at": "2026-11-04T13:55:43.693Z"
}

Pour réessayer, envoyez avec une nouvelle Idempotency-Key — l’ancienne renvoie cette tâche échouée.

Erreurs à l’envoi

Aucune ne crée de tâche ni ne facture quoi que ce soit.

422 text_not_found — un from sans box n’a rien trouvé. details.texts est ce que l’OCR a trouvé, pour que vous choisissiez une boîte :

{
  "error": {
    "code": "text_not_found",
    "message": "No text matched \"WINTER\".",
    "details": {
      "unmatched": ["WINTER"],
      "texts": [
        { "id": "t1", "text": "SALE", "box": [120, 80, 300, 60] },
        { "id": "t2", "text": "50% OFF", "box": [140, 160, 260, 50] },
        { "id": "t3", "text": "Summer Collection", "box": [100, 700, 800, 80] }
      ]
    }
  }
}

400 invalid_image — pas un JPEG, PNG ou WebP, trop gros, ou l’URL n’a pas pu être téléchargée :

{ "error": { "code": "invalid_image", "message": "Unsupported format. Send JPEG, PNG or WebP.", "details": {} } }

400 invalid_request — par exemple deux entrées d’image à la fois :

{ "error": { "code": "invalid_request", "message": "Send exactly one image input; got image_id, image_base64.", "details": {} } }

402 insufficient_balance (ou insufficient_ocr_balance sur POST /ocr) :

{ "error": { "code": "insufficient_balance", "message": "No edits left. Buy an edit pack.", "details": {} } }

429 rate_limited et 503 queue_full arrivent avec un en-tête Retry-After en secondes entières :

HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{ "error": { "code": "rate_limited", "message": "Too many requests. Retry in 12 seconds.", "details": {} } }

404 not_found — l’id de modification est erroné ou appartient à un autre compte :

{ "error": { "code": "not_found", "message": "No edit with this id.", "details": {} } }

Compte — GET /account → 200

{
  "edits_remaining": 58,
  "ocr_remaining": 68,
  "packs": [
    { "kind": "edits", "remaining": 58, "expires_at": "2027-11-04T12:05:36.775Z" },
    { "kind": "ocr", "remaining": 68, "expires_at": "2027-11-04T12:05:36.775Z" }
  ],
  "limits": { "requests_per_minute": 30, "read_requests_per_minute": 240, "concurrent_jobs": 10, "max_queued_jobs": 30 }
}

Sur cette page