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.
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;
}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.pngA: saved edit_68805a1cefad_A.jpg
B: saved edit_68805a1cefad_B.jpg
C: saved edit_68805a1cefad_C.jpg
charged 1, 59 edits leftChaque 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 }
}Démarrage rapide
Remplacez le texte de votre première image avec l’API ReWords.AI — OCR, envoi de la modification et récupération des résultats en trois requêtes curl.
Authentification
Créez des clés d’API ReWords.AI sur le site, envoyez-les comme jeton bearer et protégez vos clés et votre secret de webhook.