ReWords.AI API

サンプル

ReWords.AI API の完全な Node.js と Python のクライアント、そして出会うすべての応答(OCR、待機中・処理中・成功・一部失敗・失敗の編集、各エラー)。

完全なクライアント

どちらのスクリプトも、ローカルのファイルをアップロードし、ポーリングのルールに従ってジョブを待ち、 Retry-After を守り、ネットワークエラーや 5xx の後は同じ Idempotency-Key で再送し(ジョブが二重に作成・課金されることはありません)、 成功した結果をすべて保存し、from が一致しないときは画像内で見つかったテキストを表示します。

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

すべての応答を順番に

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 }
}

このページの内容