サンプル
ReWords.AI API の完全な Node.js と Python のクライアント、そして出会うすべての応答(OCR、待機中・処理中・成功・一部失敗・失敗の編集、各エラー)。
完全なクライアント
どちらのスクリプトも、ローカルのファイルをアップロードし、ポーリングのルールに従ってジョブを待ち、
Retry-After を守り、ネットワークエラーや 5xx の後は同じ Idempotency-Key で再送し(ジョブが二重に作成・課金されることはありません)、
成功した結果をすべて保存し、from が一致しないときは画像内で見つかったテキストを表示します。
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 leftすべての応答を順番に
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
}24 時間以内に同じ画像をもう一度送ると、新しい image_id と "charged_ocr": 0 が返ります。テキストのない画像は
"texts": [] を返し、それでも課金されます。
送信 — 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 には 50% OFF について見つかったボックスが入ります。balance ではこのジョブの編集 1 回分がすでに確保されています。
ポーリング — GET /edits/{id} → 200(実行中)
{
"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"
}成功
{
"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"
}成功したが結果の 1 つが失敗 — 課金されます
A と B のどちらか 1 つが成功すれば十分です。ここでは A が失敗したので 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"
}失敗 — 課金されません
{
"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"
}もう一度試すには、新しい Idempotency-Key で送信してください。古いキーではこの失敗したジョブが返ります。
送信時のエラー
どれもジョブを作らず、課金もされません。
422 text_not_found — box のない from が何にも一致しませんでした。details.texts は OCR で見つかったものなので、ボックスを選べます:
{
"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 — JPEG、PNG、WebP ではない、大きすぎる、または URL をダウンロードできない:
{ "error": { "code": "invalid_image", "message": "Unsupported format. Send JPEG, PNG or WebP.", "details": {} } }400 invalid_request — たとえば画像の入力を 2 つ同時に送った場合:
{ "error": { "code": "invalid_request", "message": "Send exactly one image input; got image_id, image_base64.", "details": {} } }402 insufficient_balance(POST /ocr では insufficient_ocr_balance):
{ "error": { "code": "insufficient_balance", "message": "No edits left. Buy an edit pack.", "details": {} } }429 rate_limited と 503 queue_full には、整数の秒数の Retry-After ヘッダーが付きます:
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 — 編集 id が間違っているか、別のアカウントのものです:
{ "error": { "code": "not_found", "message": "No edit with this id.", "details": {} } }アカウント — 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 }
}