Geração de imagens#
Intermediador multi-modelo de geração de imagens. Os controles de criação são
campos do request — não prosa dentro do prompt — e uma camada de prompt
enhancement os expande no prompt final, que a resposta sempre devolve em
revised_prompt. O que o modelo viu nunca fica escondido de você.
A chave do provedor é resolvida por conta: a sua credencial OPENAI_API_KEY
no cofre do Console tem precedência; sem nenhuma chave disponível a resposta é
400 IMAGE_KEY_REQUIRED.
Permissão: gerar e apagar exigem papel owner ou admin (cada geração gasta
crédito real do provedor). Ler o catálogo e o histórico é liberado para qualquer
membro autenticado.
GETCatálogo de modelos#
GET /v1/images/models — a fonte da qual você monta um seletor dinâmico, sem
fixar ids no seu código. Resposta 200:
{
"models": [
{ "id": "gpt-image-2", "label": "GPT Image 2", "description": "…",
"provider": "openai", "supports_references": true,
"sizes": ["1024x1024", "1536x1024", "1024x1536"],
"qualities": ["draft", "standard", "high"],
"formats": ["png", "jpg", "webp"], "default": true }
],
"default": "gpt-image-2",
"style_presets": [ { "id": "fotografia-editorial-realista", "label": "…", "description": "…" } ],
"aspects": ["square", "portrait", "landscape"],
"qualities": ["draft", "standard", "high"],
"formats": ["png", "jpg", "webp"],
"max_images": 4,
"max_references": 4
}
POSTGerar imagem#
POST /v1/images/generations — owner/admin. Um render em alta qualidade leva
de 30 a 90 s: ajuste o timeout do seu cliente (o padrão de 30 s aborta depois de
o provedor já ter gerado e cobrado a imagem).
{
"model": "gpt-image-2",
"prompt": "capa editorial sobre controle fiscal para MEI",
"aspect": "landscape",
"size": "1536x1024",
"quality": "high",
"output_format": "png",
"n": 1,
"style_preset": "fotografia-editorial-realista",
"brand": { "colors": ["#0FA958", "#181818"], "tone": ["confiável", "direto"] },
"reference_images": [{ "b64": "…" }, { "url": "https://…" }],
"enhance_prompt": true,
"include_b64": true
}
| Campo | Regra |
|---|---|
prompt |
obrigatório, até 4000 caracteres |
model |
opcional → o default do catálogo. Desconhecido → 400 |
aspect |
square | portrait | landscape (default square) |
size |
preset de pixels; vence aspect quando presente |
quality |
draft | standard | high (default standard) |
output_format |
png | jpg | webp (default png) |
n |
1 a 4. Acima do teto é rejeitado, nunca ajustado em silêncio — você não é cobrado por algo diferente do que pediu |
style_preset |
um dos style_presets do catálogo |
brand.colors |
hex literal (#0FA958); texto livre é recusado |
brand.tone |
até 8 descritores de até 40 caracteres |
reference_images |
até 4; b64 (data-URI aceito) ou url https pública. Só png/jpeg/webp, até 8 MiB cada |
enhance_prompt |
default true; false envia o seu prompt literal |
include_b64 |
default true; false devolve só a URL assinada (resposta muito menor) |
Resposta 200:
{
"id": "…", "model_used": "gpt-image-2", "provider": "openai",
"prompt": "capa editorial sobre controle fiscal para MEI",
"revised_prompt": "capa editorial … ; photorealistic professional quality; no text, no logos; …",
"enhanced": true,
"aspect": "landscape", "size": "1536x1024", "quality": "high", "output_format": "png",
"style_preset": "fotografia-editorial-realista",
"brand": { "colors": ["#0FA958"], "tone": ["confiável"] },
"n": 1, "reference_count": 0, "key_source": "vault",
"usage": { "input_tokens": 22, "output_tokens": 1568, "total_tokens": 1590 },
"duration_ms": 41230, "created_at": "2026-07-27T22:15:00Z",
"images": [
{ "b64_json": "…", "url": "https://…assinada…", "asset_id": "…",
"mime_type": "image/png", "byte_size": 1284339 }
]
}
A url é assinada e de vida curta — mintada de novo a cada leitura. Não a
armazene como link permanente; guarde o id da geração e releia quando precisar.
POSTAcompanhar a geração ao vivo (SSE)#
POST /v1/images/generations/stream — owner/admin. Mesmo corpo, mesmas
validações, mesmo rate limiter e mesmo custo do POST /v1/images/generations; a
diferença é que ele relata o trabalho enquanto acontece, em
text/event-stream.
Existe porque a espera é longa e era opaca. Medido contra gpt-image-2: a geração
leva cerca de 59 s, o primeiro rascunho fica pronto aos ~16 s, e o prompt final
é conhecido antes de o provedor ser chamado (o enriquecimento é determinístico e
local). Sem streaming, você não tem nada para mostrar ao seu usuário por um minuto.
Os eventos#
| Evento | Quando | Conteúdo |
|---|---|---|
prompt |
imediato (~0,4 s) | final_prompt, original_prompt, enhanced, model, size, quality, output_format, mime_type, partials_expected |
partial |
a cada rascunho | index, total, b64_json — uma imagem completa e exibível, não um pedaço |
done |
ao terminar | o mesmo objeto que o endpoint JSON devolve (id, model_used, images[], …) |
error |
falha depois do 200 | error_code, message |
event: prompt
data: {"final_prompt":"um cubo vermelho…; photorealistic professional quality…","original_prompt":"um cubo vermelho","enhanced":true,"model":"gpt-image-2","size":"1024x1024","quality":"medium","output_format":"png","mime_type":"image/png","partials_expected":3}
event: partial
data: {"index":0,"total":3,"b64_json":"iVBORw0KGgo…"}
event: done
data: {"id":"d6455bd0-…","model_used":"gpt-image-2","size":"1024x1024","images":[{"url":"https://…assinada…","asset_id":"…","mime_type":"image/png"}]}
Exemplo mínimo — curl#
curl -N -X POST https://agents-api.catcher.one/v1/images/generations/stream \
-H "X-API-Key: ctc_sua_chave" \
-H 'Content-Type: application/json' \
-d '{"prompt":"um cubo azul sobre fundo branco","quality":"draft","n":1,"include_b64":false}'
O -N (sem buffer) é obrigatório: sem ele o curl segura a saída e você só vê tudo
no final, que é exatamente o que o streaming existe para evitar.
Node / TypeScript#
const res = await fetch('https://agents-api.catcher.one/v1/images/generations/stream', {
method: 'POST',
headers: { 'X-API-Key': process.env.CATCHER_API_KEY!, 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: 'um cubo azul sobre fundo branco', n: 1, include_b64: false }),
})
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`)
const reader = res.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''
for (;;) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
// Os frames são separados por linha em branco. Guarde o resto: um frame de
// ~1,5 MB chega em vários chunks e cortá-lo cedo quebra o JSON.
const frames = buffer.split('\n\n')
buffer = frames.pop() ?? ''
for (const frame of frames) {
const evLine = frame.split('\n').find((l) => l.startsWith('event:'))
const dataLine = frame.split('\n').find((l) => l.startsWith('data:'))
if (!evLine || !dataLine) continue
const event = evLine.slice(6).trim()
const data = JSON.parse(dataLine.slice(5).trim())
if (event === 'prompt') console.log('prompt final:', data.final_prompt)
if (event === 'partial') render(`data:${'image/png'};base64,${data.b64_json}`)
if (event === 'done') console.log('pronto:', data.images[0].url)
if (event === 'error') throw new Error(`${data.error_code}: ${data.message}`)
}
}
EventSource não serve aqui — ele só faz GET e não envia cabeçalhos, então
não dá para mandar o corpo nem a X-API-Key. Use fetch + reader, como acima.
Python#
import json, requests
with requests.post(
"https://agents-api.catcher.one/v1/images/generations/stream",
headers={"X-API-Key": API_KEY},
json={"prompt": "um cubo azul sobre fundo branco", "n": 1, "include_b64": False},
stream=True, timeout=300,
) as res:
res.raise_for_status()
event = None
for line in res.iter_lines(decode_unicode=True):
if line is None or line == "":
continue
if line.startswith("event:"):
event = line.split(":", 1)[1].strip()
elif line.startswith("data:"):
data = json.loads(line[5:].strip())
if event == "prompt":
print("prompt final:", data["final_prompt"])
elif event == "partial":
print("rascunho", data["index"] + 1, "de", data["total"])
elif event == "done":
print("pronto:", data["images"][0]["url"])
elif event == "error":
raise RuntimeError(f'{data["error_code"]}: {data["message"]}')
O que você precisa saber antes de integrar#
Não há percentual. O provedor não emite nenhum e os rascunhos chegam em
intervalos irregulares (medidos: 15,6 s, 40,1 s, 47,6 s). index/total é um
fato; um percentual seria o nosso relógio adivinhando. Se sua UI precisa de uma
barra, use os rascunhos recebidos como estágios.
partials_expected pode vir 0, e você deve respeitar. Com n > 1 o
provedor recusa streaming, então a geração acontece pelo caminho normal e nenhum
partial é enviado — você recebe prompt e depois done. O campo avisa disso
no primeiro evento; não fique esperando rascunhos que não vêm. Referências de
estilo também caem nesse caso.
Cancelar é fechar a conexão. O contexto do servidor cancela junto, a chamada ao provedor é abortada e nada é persistido — a geração não aparece no histórico e você não paga por ela do nosso lado.
Falhas com status acontecem ANTES de o stream abrir. 401/403 de
credencial, 400 IMAGE_INVALID_REQUEST, 400 IMAGE_KEY_REQUIRED, 402 de
orçamento, 501 IMAGE_GEN_DISABLED chegam como resposta HTTP normal — sempre
cheque res.ok antes de começar a ler. Depois do 200, só existe o evento
error (tipicamente uma queda do provedor no meio).
Peso. Cada partial carrega ~1,5 MB de base64 (a imagem inteira). Três
rascunhos são ~4,5 MB extras por geração. Se isso pesar no seu cliente, use o
endpoint JSON POST /v1/images/generations, que devolve só o resultado.
É uma API de servidor, não de browser. A X-API-Key é uma credencial de
servidor e o CORS não libera origens de terceiros — chame do seu backend e
repasse ao seu front pelo seu próprio canal. Colocar a chave no browser a expõe a
qualquer visitante.
Timeout do seu cliente. Uma geração em alta qualidade passa de 90 s; deixe o timeout do seu HTTP client em pelo menos 300 s ou você abortará a chamada que já está sendo cobrada.
GETHistórico e download#
GET /v1/images/generations?page=1&limit=20 — as gerações da conta, mais recente
primeiro: { generations: [...], total, page, limit }. Os itens do histórico vêm
sem b64_json (só as URLs assinadas).
GET /v1/images/generations/{id} — uma geração, com URLs assinadas frescas. Id
inexistente ou de outra conta responde 404 IMAGE_NOT_FOUND — os dois casos
são indistinguíveis de propósito.
DELETE /v1/images/generations/{id} — owner/admin, 204. Remove o registro e
os bytes armazenados.