convertajá

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.

10 min · Intermediário · atualizado em 16/10/2026

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

RespostaRepetir?
429 rate_limitedSim, depois dos segundos de Retry-After. A chamada foi recusada antes de cobrar.
429 concurrency_limitSim, com espera crescente (uma tarefa pesada precisa terminar).
429 test_limitNão: a chave de teste só volta no dia seguinte.
502 upstream_unavailable, 503Sim, com espera crescente. Nada foi cobrado.
500 internal_errorEm GET, sim. Em POST, confira antes se a tarefa/envelope foi criado.
400, 401, 402, 404, 413, 415, 422Nã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());

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=true com 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 o id e consulte ou use webhook.
  • Grave o id antes 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 id do evento e ignore duplicados (veja Webhooks).

Passo 3: Respeite os limites

  • Por minuto, por chave: janela deslizante de 60 s. Acompanhe X-RateLimit-Remaining e, 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_concurrency por conta. Use uma fila com esse número de trabalhadores.
  • Tamanho e quantidade: limits.max_file_bytes e limits.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çãoCréditos
Ferramenta leve (unir, dividir, comprimir, DANFE, recortar etiqueta, imagens…), por arquivo1
Ferramenta pesada (OCR, Word/Excel/PowerPoint, PDF/A, HTML para PDF…), por arquivo de até 10 páginas5 + 1 a cada 10 páginas a mais
Vídeo (converter, comprimir, GIF), por arquivo10
Consulta de CNPJ ou CEPgrátis
Validação de documentos (lote: a cada 50 itens), boleto, Pix, QR Code, código de barras1
Assinatura eletrônica, por envelope enviado20
Tarefa que termina com erro0 (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 em GET /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"
Resposta 200 (trecho)
{
  "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-Id salvo no seu log de cada chamada.
  • Novas tentativas só para 429/502/503, respeitando Retry-After.
  • id da 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.