Guia 9 de 10
Idempotência, lista de tarefas e reenvios seguros
Rede cai, servidor do cliente reinicia, fila reprocessa. Com a Idempotency-Key, repetir um POST nunca cria outra tarefa nem cobra duas vezes; com GET /v1/jobs, você reencontra qualquer tarefa dos últimos 90 dias.
O que você vai construir
Uma função que cria tarefas com novas tentativas seguras (mesma chave em todas), uma varredura paginada das tarefas do dia com o gasto de créditos e a limpeza dos arquivos sensíveis logo depois do download.
Antes de começar
- Uma chave de API em
CONVERTAJA_KEY(veja Primeiros passos). - Já ter criado uma tarefa (veja Converter arquivos).
Como a idempotência funciona
- Vale para todo
POSTque cria algo ou cobra:/v1/tools/{id},/v1/esign/envelopes,/v1/esign/envelopes/{id}/send,/v1/validate,/v1/boleto/parse,/v1/pix/brcode,/v1/qrcodee/v1/barcode. - Mesma chave + mesmo pedido (em até 24 h): a resposta original volta igual (mesmo status, mesmo
id), com o cabeçalhoIdempotent-Replayed: true. Nada é cobrado de novo. - Mesma chave + pedido diferente:
409 idempotency_key_reused. Comparamos caminho, campos, opções e o SHA-256 de cada arquivo (o separador do multipart pode mudar à vontade). - Ao mesmo tempo: se duas tentativas chegam juntas, só uma roda; a outra espera e recebe a mesma resposta (ou
409 idempotency_in_progresscomRetry-After, se passar de ~60 s). - Erros não ficam guardados: depois de
402,429ou400, corrija e repita com a mesma chave. - A chave vale por conta (produção e teste separadas), tem até 255 caracteres visíveis e expira em 24 horas.
Passo 1: Crie a tarefa com Idempotency-Key
Gere a chave antes da primeira tentativa e guarde junto do pedido no seu banco (ex.: pedido_123:comprimir ou um UUID). Todas as tentativas usam a mesma chave:
# Gere UMA chave por operação e repita o MESMO pedido com a MESMA chave se a conexão cair
CHAVE=$(uuidgen) # ex.: 6f1c2b8e-3f0a-4c55-9d1e-2a7b9c0d4e11; guarde junto do pedido no seu banco
curl -X POST "https://convertaja.com.br/v1/tools/comprimir-pdf" \
-H "Authorization: Bearer $CONVERTAJA_KEY" \
-H "Idempotency-Key: $CHAVE" \
-F "files=@contrato.pdf" \
--retry 3 --retry-all-errors -i
# A repetição devolve a mesma tarefa (mesmo "id") com o cabeçalho Idempotent-Replayed: true, sem cobrar de novoimport { randomUUID } from 'node:crypto';
import { openAsBlob } from 'node:fs';
// Cria a tarefa com novas tentativas seguras: a mesma Idempotency-Key em todas as tentativas
async function criarTarefa(arquivo, chave = randomUUID()) {
for (let tentativa = 1; tentativa <= 4; tentativa++) {
try {
const form = new FormData();
form.append('files', await openAsBlob(arquivo), arquivo);
const res = await fetch('https://convertaja.com.br/v1/tools/comprimir-pdf', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.CONVERTAJA_KEY}`, 'Idempotency-Key': chave },
body: form,
signal: AbortSignal.timeout(30000),
});
const corpo = await res.json();
if (res.status === 409 && corpo.error.code === 'idempotency_in_progress') throw new Error('em andamento');
if (!res.ok) throw Object.assign(new Error(corpo.error.message), { fatal: true, code: corpo.error.code });
if (res.headers.get('idempotent-replayed')) console.log('Repetição: a tarefa já existia, nada foi cobrado de novo.');
return corpo; // { id, status: "running", credits_charged, … }
} catch (e) {
if (e.fatal || tentativa === 4) throw e;
await new Promise((r) => setTimeout(r, 1000 * 2 ** tentativa)); // queda de rede ou tempo esgotado: tenta de novo
}
}
}
const tarefa = await criarTarefa('contrato.pdf');
console.log(tarefa.id, tarefa.status);import os
import time
import uuid
import requests
def criar_tarefa(arquivo, chave=None):
chave = chave or str(uuid.uuid4()) # a mesma chave em todas as tentativas
for tentativa in range(1, 5):
try:
with open(arquivo, "rb") as f:
res = requests.post(
"https://convertaja.com.br/v1/tools/comprimir-pdf",
headers={"Authorization": f"Bearer {os.environ['CONVERTAJA_KEY']}", "Idempotency-Key": chave},
files={"files": f},
timeout=30,
)
corpo = res.json()
if res.status_code == 409 and corpo["error"]["code"] == "idempotency_in_progress":
raise requests.ConnectionError("em andamento")
if not res.ok:
raise SystemExit(f"{corpo['error']['code']}: {corpo['error']['message']}")
if res.headers.get("Idempotent-Replayed"):
print("Repetição: a tarefa já existia, nada foi cobrado de novo.")
return corpo
except (requests.ConnectionError, requests.Timeout):
if tentativa == 4:
raise
time.sleep(2 ** tentativa)
tarefa = criar_tarefa("contrato.pdf")
print(tarefa["id"], tarefa["status"])
HTTP/1.1 202 Accepted
Idempotent-Replayed: true
Location: /v1/jobs/Fi_TgsjgpM19
X-Credits-Remaining: 29874
{ "id": "Fi_TgsjgpM19", "tool": "comprimir-pdf", "status": "running", "credits_charged": 1, … }{
"error": {
"code": "idempotency_key_reused",
"message": "Esta Idempotency-Key já foi usada nas últimas 24 horas em outro pedido (POST /v1/tools/comprimir-pdf com outro conteúdo). Use uma chave nova para cada operação diferente.",
"request_id": "req_UkbrW3QRi59d"
}
}Dica A resposta repetida é a original (de quando a tarefa foi criada). Para a situação atual, consulte GET /v1/jobs/{id}.
Passo 2: Liste e filtre as tarefas
GET /v1/jobs devolve as tarefas da conta, da mais recente para a mais antiga, por 90 dias (os arquivos ficam 1 hora: files_available diz se ainda dá para baixar). Filtros:
| Parâmetro | Valores |
|---|---|
limit | 1 a 100 (padrão 20) |
starting_after | o next_cursor da página anterior (id da última tarefa) |
status | running, done, error (vários com vírgula: done,error) |
tool | id da ferramenta, ex.: comprimir-pdf |
created_after, created_before | ISO 8601 (2026-10-17 = meia-noite de Brasília, ou 2026-10-17T13:00:00Z) ou Unix em segundos |
# Tarefas concluídas de uma ferramenta, criadas a partir de 17/10/2026 (horário de Brasília), 50 por página
curl -G "https://convertaja.com.br/v1/jobs" \
-H "Authorization: Bearer $CONVERTAJA_KEY" \
--data-urlencode "status=done" \
--data-urlencode "tool=comprimir-pdf" \
--data-urlencode "created_after=2026-10-17" \
--data-urlencode "limit=50"
# Próxima página: o next_cursor da resposta em starting_after
curl -G "https://convertaja.com.br/v1/jobs" -H "Authorization: Bearer $CONVERTAJA_KEY" \
--data-urlencode "limit=50" --data-urlencode "starting_after=Fi_TgsjgpM19"// Percorre todas as páginas de GET /v1/jobs (mais recentes primeiro)
async function* tarefas(filtros = {}) {
let cursor = null;
do {
const qs = new URLSearchParams({ limit: '100', ...filtros, ...(cursor ? { starting_after: cursor } : {}) });
const res = await fetch(`https://convertaja.com.br/v1/jobs?${qs}`, { headers: { Authorization: `Bearer ${process.env.CONVERTAJA_KEY}` } });
const pagina = await res.json();
if (!res.ok) throw new Error(`${pagina.error.code}: ${pagina.error.message}`);
yield* pagina.data;
cursor = pagina.has_more ? pagina.next_cursor : null;
} while (cursor);
}
const desde = new Date(Date.now() - 24 * 3600 * 1000).toISOString();
let creditos = 0;
for await (const t of tarefas({ status: 'done,error', created_after: desde })) {
creditos += t.credits_charged - t.credits_refunded;
console.log(t.created_at, t.tool, t.status, t.files_available ? t.download_url : '(arquivos já apagados)');
}
console.log('Créditos nas últimas 24 h:', creditos);import os
from datetime import datetime, timedelta, timezone
import requests
def tarefas(**filtros):
"""Percorre todas as páginas de GET /v1/jobs (mais recentes primeiro)."""
cursor = None
while True:
params = {"limit": 100, **filtros, **({"starting_after": cursor} if cursor else {})}
res = requests.get("https://convertaja.com.br/v1/jobs", params=params,
headers={"Authorization": f"Bearer {os.environ['CONVERTAJA_KEY']}"}, timeout=30)
pagina = res.json()
if not res.ok:
raise SystemExit(f"{pagina['error']['code']}: {pagina['error']['message']}")
yield from pagina["data"]
if not pagina["has_more"]:
break
cursor = pagina["next_cursor"]
desde = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
for t in tarefas(status="error", created_after=desde):
print(t["created_at"], t["tool"], t["error"]["code"], t["error"]["message"])
{
"object": "list",
"count": 2,
"has_more": true,
"next_cursor": "lrzqWwqfYv4a",
"data": [
{
"id": "Fi_TgsjgpM19",
"tool": "comprimir-pdf",
"operation": "pdf-compress",
"status": "done",
"progress": 100,
"message": "Pronto!",
"created_at": "2026-10-16T20:36:06.144Z",
"finished_at": "2026-10-16T20:36:07.523Z",
"expires_at": "2026-10-16T21:36:06.144Z",
"credits_charged": 1,
"credits_refunded": 0,
"test": false,
"outputs": [
{
"index": 0,
"name": "contrato - comprimido.pdf",
"size": 34031,
"url": "https://convertaja.com.br/v1/jobs/Fi_TgsjgpM19/files/0"
}
],
"download_url": "https://convertaja.com.br/v1/jobs/Fi_TgsjgpM19/download",
"info": "5.5 MB → 33 KB (99% menor)",
"files_available": true,
"files_deleted_at": null
},
{
"id": "lrzqWwqfYv4a",
"tool": "xml-nfe-para-pdf",
"operation": "nfe-danfe",
"status": "error",
"progress": 0,
"message": "Nenhuma NF-e encontrada. Envie os arquivos XML das notas (ou um ZIP com eles). (1 arquivo ignorado por não ser NF-e)",
"created_at": "2026-10-16T20:24:25.824Z",
"finished_at": "2026-10-16T20:24:25.998Z",
"expires_at": "2026-10-16T21:24:25.824Z",
"credits_charged": 1,
"credits_refunded": 1,
"test": false,
"outputs": [],
"error": {
"code": "job_failed",
"message": "Nenhuma NF-e encontrada. Envie os arquivos XML das notas (ou um ZIP com eles). (1 arquivo ignorado por não ser NF-e)"
},
"files_available": false,
"files_deleted_at": null
}
]
}Passo 3: Apague os arquivos assim que baixar
Documentos sensíveis (contratos, notas, exames)? Não espere 1 hora: depois de baixar, apague. A tarefa continua na lista, com files_available: false e files_deleted_at. Tarefa ainda em andamento: 409 job_running (não há cancelamento no meio; apague quando terminar).
# Apaga os arquivos gerados antes de 1 hora (a tarefa continua na lista, com files_available: false)
curl -X DELETE "https://convertaja.com.br/v1/jobs/Fi_TgsjgpM19" \
-H "Authorization: Bearer $CONVERTAJA_KEY"
# Mesmo efeito, para clientes que só fazem GET e POST
curl -X POST "https://convertaja.com.br/v1/jobs/Fi_TgsjgpM19/delete" \
-H "Authorization: Bearer $CONVERTAJA_KEY"
Tarefas interrompidas
Se o servidor reiniciar no meio de uma tarefa, ela termina com status: "error" e error.code: "job_interrupted", e os créditos voltam automaticamente. Envie de novo com uma nova Idempotency-Key (a resposta guardada da chave antiga é a da tarefa interrompida).
Próximos passos
Todos os endpoints e opções: referência da API. Dúvidas? Fale com a gente e informe o X-Request-Id da chamada.