1

Como cobrar por uso numa API: contagem, limites e Stripe

Quando o custo da API varia com o uso, um plano mensal fixo faz quem usa pouco subsidiar quem usa muito. Cobrar por uso resolve isso, mas exige três peças: contar o uso por cliente, limitar quem passa do combinado e levar o total para a fatura. Este post mostra como montar as três com Express e Stripe.

1. Escolha o que contar

Antes de qualquer código, decida a unidade: requisições, registros processados, tokens, segundos de processamento. Uma boa unidade cresce junto com o valor que o cliente recebe, acompanha o seu custo e pode ser conferida pelo próprio cliente.

Uma regra prática: cobre por requisição, a não ser que as chamadas mais caras custem várias vezes mais que as baratas. Nesse caso, dê um peso maior para a rota cara ou cobre por unidade (tokens, por exemplo).

2. Conte por cliente, dentro do processo

O middleware abaixo identifica o cliente pela chave de API, deixa o handler mudar quantas unidades a requisição vale e só conta depois que a resposta sai com sucesso:

import crypto from "node:crypto";

// "clienteId|2026-11-09T14" -> unidades daquela hora
const contagens = new Map();

export function medirUso(buscarClientePorHash) {
  return async (req, res, next) => {
    const auth = req.get("authorization") ?? "";
    const chave = auth.startsWith("Bearer ") ? auth.slice(7) : req.get("x-api-key");
    if (!chave) return res.status(401).json({ error: "missing_api_key" });

    // Guarde só o hash da chave no banco, nunca a chave em si
    const hash = crypto.createHash("sha256").update(chave).digest("hex");
    const cliente = await buscarClientePorHash(hash);
    if (!cliente) return res.status(401).json({ error: "invalid_api_key" });

    req.cliente = cliente;
    req.unidades = 1; // o handler pode trocar, ex.: req.unidades = resultado.tokens

    res.on("finish", () => {
      if (res.statusCode >= 400) return; // erro não entra na conta
      const hora = new Date().toISOString().slice(0, 13);
      const k = `${cliente.id}|${hora}`;
      contagens.set(k, (contagens.get(k) ?? 0) + req.unidades);
    });
    next();
  };
}

E a cada minuto o processo grava o que juntou:

setInterval(async () => {
  const lote = [...contagens];
  contagens.clear();
  for (const [k, unidades] of lote) {
    const [clienteId, hora] = k.split("|");
    await db.query(
      `insert into uso (cliente_id, hora, unidades) values ($1, $2, $3)
       on conflict (cliente_id, hora) do update set unidades = uso.unidades + excluded.unidades`,
      [clienteId, `${hora}:00:00Z`, unidades],
    );
  }
}, 60_000);

Por que assim:

  • Contar em memória e gravar em lote evita uma escrita no banco por requisição. O preço é que, se o processo morrer com kill -9 ou falta de memória, o último minuto se perde. Trate o SIGTERM e grave antes de sair. Se o insert falhar, devolva o lote para o Map em vez de descartar.
  • Agrupar por hora em vez de por mês permite somar o uso de um período que não começa no dia 1, como costuma ser o da assinatura no Stripe. A soma é por horas inteiras: se o período começar no meio de uma hora, este exemplo não separa o uso de antes e de depois. Escolha uma regra (a hora vai para o período em que começou, por exemplo) ou agrupe por minuto.
  • Não cobrar 4xx e 5xx evita discussão. Cliente que recebe erro e ainda paga por ele abre ticket.

3. Limite por cliente

São dois limites diferentes, e vale ter os dois:

  • Por segundo ou minuto, para que o job em lote de um cliente não derrube os outros. Token bucket por cliente resolve. Ao recusar, responda 429 com o cabeçalho Retry-After dizendo quantos segundos esperar.
  • Por mês (cota), ligado ao plano. Aqui você precisa decidir o que acontece quando o cliente chega no limite: parar (bom para plano grátis), cobrar o excedente no fim do mês, ou descontar de créditos pré-pagos.

Cuidado com mais de uma instância: um contador em memória em cada processo deixa o cliente fazer o limite vezes o número de instâncias. Ou o contador fica num lugar compartilhado (Redis, banco), ou você divide o limite pelo número de instâncias e aceita a imprecisão.

4. Leve o total para o Stripe

No Stripe, o uso entra por um meter (Billing Meters): você cria o meter, um preço ligado a ele e uma assinatura com esse preço. Depois manda eventos com o uso de cada cliente. Para um plano mensal com uso incluído, o mais simples é mandar um evento por cliente por período, depois que o período fecha:

await stripe.billing.meterEvents.create({
  event_name: "api_requests",
  payload: {
    stripe_customer_id: cliente.stripeCustomerId,
    value: String(total), // os valores do payload são strings
  },
  identifier: `${cliente.stripeCustomerId}_${inicioDoPeriodo}`, // igual em toda nova tentativa
  timestamp: fimDoPeriodo - 1, // último segundo do período
});

O total é a soma da tabela uso entre current_period_start e current_period_end da assinatura.

Detalhes que dão problema:

  • identifier fixo. O Stripe garante unicidade do identifier numa janela de pelo menos 24 horas, então uma nova tentativa com o mesmo valor conta uma vez só. Se você gerar um UUID novo a cada tentativa, cobra duas vezes.
  • timestamp tem que estar nos últimos 35 dias e no máximo 5 minutos no futuro. É ele que decide em qual período o uso cai.
  • Período de carência. Quando o período acaba, a fatura fica em rascunho por 1 hora por padrão (dá para aumentar até 72 horas nas configurações de fatura). Uso enviado depois que a fatura fecha não entra nela nem na próxima. Se o seu job roda horas depois do fim do período, aumente a carência.
  • Erros chegam depois. O Stripe processa os eventos de forma assíncrona; alguns erros só aparecem como eventos v1.billing.meter.error_report_triggered. Escute esse webhook.
  • Faixas graduated, não volume. Com volume, o preço da faixa onde o total cai vale para todas as unidades, e um cliente pode pagar menos usando mais.

Um aviso: o Stripe hoje recomenda o Metronome para integrações novas de cobrança por uso e diz que os meters continuam com suporte completo. Vale ler a comparação na documentação do Stripe antes de escolher.

5. Mostre o uso para o cliente

Uma fatura correta ainda gera reclamação se vier o dobro do esperado. Dê ao cliente uma página com o uso do período, o que falta da cota e uma estimativa da próxima fatura, e mande alerta quando chegar em 80% do incluído. Alimente essa página com a sua tabela uso, não com o Stripe: com um evento por período, a prévia de fatura do Stripe só mostra o uso depois que o período fecha.


Transparência: eu trabalho no UsageRail, que faz essas partes (chaves, limites, cotas e envio do uso para o Stripe da sua conta) como um SDK de usage-based billing for APIs para Node.js e Python. O código acima funciona sem ele.

Carregando publicação patrocinada...