Guia 8 de 8
Boas práticas: reenvios, limites, segurança e custos
O que separa um teste que funciona de uma integração que roda meses sem surpresa: repetir só o que pode ser repetido, respeitar os limites, proteger a chave e saber quanto cada chamada custa.
O que você vai construir
Uma função de chamada com novas tentativas (respeitando Retry-After) e um checklist para produção.
Antes de começar
- Já ter feito pelo menos uma integração dos outros guias.
Passo 1: Repita só o que pode ser repetido
| Resposta | Repetir? |
|---|---|
429 rate_limited | Sim, depois dos segundos de Retry-After. A chamada foi recusada antes de cobrar. |
429 concurrency_limit | Sim, com espera crescente (uma tarefa pesada precisa terminar). |
429 test_limit | Não: a chave de teste só volta no dia seguinte. |
502 upstream_unavailable, 503 | Sim, com espera crescente. Nada foi cobrado. |
500 internal_error | Em GET, sim. Em POST, confira antes se a tarefa/envelope foi criado. |
400, 401, 402, 404, 413, 415, 422 | Não: corrija o pedido (ou o saldo) antes. |
Tarefa com status: "error" | Só se o arquivo mudou. Os créditos já voltaram. |
// chamar(): repete a chamada quando vale a pena (429 de limite por minuto, 502/503, falha de rede)
// e devolve a resposta para os outros casos. Não repete erros 4xx de validação: eles vão falhar de novo.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function chamar(url, init = {}, { tentativas = 4, repetirRede = init.method === undefined || init.method === 'GET' } = {}) {
for (let i = 1; ; i++) {
let res;
try {
res = await fetch(url, init);
} catch (erro) {
// Sem resposta: num POST não dá para saber se a tarefa foi criada (veja "Reenvios sem cobrança dupla")
if (!repetirRede || i >= tentativas) throw erro;
}
if (res) {
const temporario = res.status === 429 || res.status === 502 || res.status === 503;
if (!temporario || i >= tentativas) return res;
const corpo = await res.clone().json().catch(() => ({}));
if (corpo.error?.code === 'test_limit') return res; // chave de teste: limite do dia, não adianta repetir
}
const retryAfter = Number(res?.headers.get('retry-after'));
await sleep(retryAfter > 0 ? retryAfter * 1000 : Math.min(30000, 1000 * 2 ** (i - 1)) + Math.random() * 300);
}
}
// Exemplo
const res = await chamar('https://convertaja.com.br/v1/cep/01310100', { headers: { Authorization: `Bearer ${process.env.CONVERTAJA_KEY}` } });
console.log(res.status, await res.json());# chamar(): repete a chamada quando vale a pena (429 de limite por minuto, 502/503, falha de rede)
# e devolve a resposta para os outros casos. Não repete erros 4xx de validação: eles vão falhar de novo.
import os
import random
import time
import requests
def chamar(method, url, tentativas=4, repetir_rede=None, **kwargs):
if repetir_rede is None:
repetir_rede = method.upper() == "GET" # num POST sem resposta não dá para saber se a tarefa foi criada
kwargs.setdefault("timeout", 60)
for i in range(1, tentativas + 1):
try:
res = requests.request(method, url, **kwargs)
except requests.ConnectionError:
if not repetir_rede or i == tentativas:
raise
res = None
if res is not None:
temporario = res.status_code in (429, 502, 503)
if not temporario or i == tentativas:
return res
if res.headers.get("Content-Type", "").startswith("application/json") and res.json().get("error", {}).get("code") == "test_limit":
return res # chave de teste: limite do dia, não adianta repetir
retry_after = int(res.headers.get("Retry-After", "0")) if res is not None else 0
time.sleep(retry_after if retry_after > 0 else min(30, 2 ** (i - 1)) + random.random() * 0.3)
# Exemplo
res = chamar("GET", "https://convertaja.com.br/v1/cep/01310100", headers={"Authorization": f"Bearer {os.environ['CONVERTAJA_KEY']}"})
print(res.status_code, res.json())<?php
// chamar(): repete a chamada quando vale a pena (429 de limite por minuto, 502/503)
// e devolve [status, corpo] nos outros casos. Não repete erros 4xx de validação: eles vão falhar de novo.
function chamar(string $url, array $opcoes = [], int $tentativas = 4): array {
for ($i = 1; ; $i++) {
$retryAfter = 0;
$ch = curl_init($url);
curl_setopt_array($ch, $opcoes + [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60]);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $linha) use (&$retryAfter) {
if (stripos($linha, 'Retry-After:') === 0) $retryAfter = (int) trim(substr($linha, 12));
return strlen($linha);
});
$corpo = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$temporario = in_array($status, [429, 502, 503], true) && ($corpo['error']['code'] ?? '') !== 'test_limit';
if (!$temporario || $i >= $tentativas) return [$status, $corpo];
sleep($retryAfter > 0 ? $retryAfter : min(30, 2 ** ($i - 1)));
}
}
// Exemplo
[$status, $cep] = chamar('https://convertaja.com.br/v1/cep/01310100', [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CONVERTAJA_KEY')],
]);
echo "$status {$cep['cidade']}\n";
Passo 2: Reenvios sem cobrança dupla
A API não tem chave de idempotência: cada POST /v1/tools/{id} aceito cria uma tarefa nova e cobra de novo, e cada POST /v1/esign/envelopes cria (e envia) outro envelope. Por isso:
- Não use
?wait=truecom timeout curto. Se a sua conexão cair esperando, a tarefa continua e já foi cobrada. Para arquivos grandes ou ferramentas pesadas, crie a tarefa (202 chega em instantes), guarde oide consulte ou use webhook. - Grave o
idantes de qualquer outra coisa. Com ele, você retoma a consulta em vez de reenviar o arquivo. - Falha de rede num POST? Você não sabe se a tarefa foi criada. Para tarefas, o custo de reenviar é pequeno e o resultado é igual; para envelopes, liste antes (
GET /v1/esign/envelopes?limit=5) e confira se ele já existe, para não mandar dois convites. - Webhooks podem chegar repetidos. Guarde o
iddo evento e ignore duplicados (veja Webhooks).
Passo 3: Respeite os limites
- Por minuto, por chave: janela deslizante de 60 s. Acompanhe
X-RateLimit-Remaininge, se estiver perto de zero, espere atéX-RateLimit-Reset. Consultas de tarefa (GET /v1/jobs/{id}) também contam: consulte a cada 1 ou 2 s, não em laço apertado. - Tarefas pesadas ao mesmo tempo:
limits.heavy_jobs_concurrencypor conta. Use uma fila com esse número de trabalhadores. - Tamanho e quantidade:
limits.max_file_byteselimits.max_files_per_job. Divida lotes grandes (como no guia NF-e em lote). - Arquivos ficam 1 hora no ar. Baixe e guarde no seu armazenamento assim que a tarefa terminar.
Todos os números do seu plano estão em GET /v1/me, em limits.
Passo 4: Proteja a chave e os avisos
- Chave só no servidor, em variável de ambiente ou cofre de segredos. Nunca no navegador, app, repositório ou log.
- Uma chave por sistema e ambiente (ex.: “ERP produção”, “site homologação”). Se uma vazar, você revoga só ela em /conta/api, na hora.
- Use chaves
cj_test_em desenvolvimento e CI: resultados reais, sem gastar créditos (até 20 chamadas por dia). - Confira sempre a assinatura dos webhooks e recuse avisos com mais de 5 minutos.
- A API só baixa arquivos e entrega avisos em endereços públicos HTTPS; redes internas são bloqueadas mesmo depois de redirecionamentos.
- Os arquivos são apagados 1 hora depois da criação da tarefa. Guardamos o registro de acesso (IP, data e hora) exigido pelo Marco Civil da Internet.
Passo 5: Controle os custos
| Operação | Créditos |
|---|---|
| Ferramenta leve (unir, dividir, comprimir, DANFE, recortar etiqueta, imagens…), por arquivo | 1 |
| Ferramenta pesada (OCR, Word/Excel/PowerPoint, PDF/A, HTML para PDF…), por arquivo de até 10 páginas | 5 + 1 a cada 10 páginas a mais |
| Vídeo (converter, comprimir, GIF), por arquivo | 10 |
| Consulta de CNPJ ou CEP | grátis |
| Validação de documentos (lote: a cada 50 itens), boleto, Pix, QR Code, código de barras | 1 |
| Assinatura eletrônica, por envelope enviado | 20 |
| Tarefa que termina com erro | 0 (estorno) |
- Tarefas são cobradas quando aceitas (202) e estornadas automaticamente se terminarem com erro.
- Consultas de dados só são cobradas quando dão certo.
- Valide em lote: até 50 documentos pelo preço de uma chamada.
- Acompanhe o saldo em
X-Credits-Remaining(toda resposta) e o gasto por dia e por endpoint emGET /v1/usage. Avisamos por e-mail quando 80% da franquia do ciclo for usada.
// Toda resposta autenticada traz o saldo: use para avisar antes de faltar crédito
const res = await fetch('https://convertaja.com.br/v1/me', { headers: { Authorization: `Bearer ${process.env.CONVERTAJA_KEY}` } });
const saldo = Number(res.headers.get('x-credits-remaining'));
if (saldo < 200) console.warn(`Atenção: restam ${saldo} créditos`);
# Créditos e chamadas por dia e por endpoint (padrão: últimos 30 dias)
curl "https://convertaja.com.br/v1/usage?from=2026-10-01&to=2026-10-31" \
-H "Authorization: Bearer $CONVERTAJA_KEY"
{
"from": "2026-10-01",
"to": "2026-10-31",
"totals": {
"credits": 149,
"calls": 144
},
"data": [
{
"day": "2026-10-16",
"credits": 149,
"calls": 144,
"endpoints": {
"tools/comprimir-pdf": {
"credits": 14,
"calls": 23
},
"tools/xml-nfe-para-pdf": {
"credits": 9,
"calls": 8
},
"validate": {
"credits": 2,
"calls": 3
},
"jobs": {
"credits": 0,
"calls": 20
}
}
}
]
}Checklist para produção
- Chave
cj_live_própria do sistema, guardada como segredo. - Tratamento de erro pelo
error.code, não pela mensagem (que pode mudar). X-Request-Idsalvo no seu log de cada chamada.- Novas tentativas só para 429/502/503, respeitando
Retry-After. idda tarefa ou do envelope gravado antes de esperar o resultado.- Webhook com assinatura conferida, resposta em menos de 10 s e eventos repetidos ignorados.
- Plano B para webhook perdido: consulta da tarefa dentro de 1 hora.
- Arquivos baixados para o seu armazenamento logo depois de prontos.
- Alerta de saldo baixo (
X-Credits-Remaining).
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.