ReWords.AI API

Exemplos

Clientes completos em Node.js e Python para a API da ReWords.AI, e todas as respostas que você vai encontrar — OCR, uma edição na fila, em processamento, concluída, com falha parcial e com falha, e cada erro.

Um cliente completo

Os dois scripts enviam um arquivo local, aguardam o job seguindo a regra de polling, respeitam Retry-After, reenviam depois de um erro de rede ou 5xx com a mesma Idempotency-Key (assim um job nunca é criado nem cobrado duas vezes), salvam todo resultado bem-sucedido e mostram o texto encontrado na imagem quando from não corresponde.

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

Cada resposta, passo a passo

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
}

A mesma imagem de novo em até 24 horas retorna um novo image_id e "charged_ocr": 0. Uma imagem sem texto retorna "texts": [] e é cobrada mesmo assim.

Enviar — 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 mostra a caixa que encontramos para 50% OFF. balance já tem a edição deste job reservada.

Consultar — GET /edits/{id} → 200, em execução

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

Concluído

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

Concluído, mas um resultado falhou — ainda cobrado

Basta um entre A e B dar certo. Aqui A falhou, então use 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"
}

Falhou — não cobrado

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

Para tentar de novo, envie com uma Idempotency-Key nova — a antiga retorna este job com falha.

Erros ao enviar

Nenhum destes cria um job ou cobra algo.

422 text_not_found — um from sem box não correspondeu a nada. details.texts é o que o OCR encontrou, para você escolher uma caixa:

{
  "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 — não é JPEG, PNG ou WebP, é grande demais ou a URL não pôde ser baixada:

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

400 invalid_request — por exemplo, duas entradas de imagem ao mesmo tempo:

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

402 insufficient_balance (ou insufficient_ocr_balance em POST /ocr):

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

429 rate_limited e 503 queue_full vêm com um cabeçalho Retry-After em segundos inteiros:

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 — o id da edição está errado ou pertence a outra conta:

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

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

Nesta página