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 -9ou falta de memória, o último minuto se perde. Trate oSIGTERMe grave antes de sair. Se o insert falhar, devolva o lote para oMapem 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
429com o cabeçalhoRetry-Afterdizendo 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:
identifierfixo. O Stripe garante unicidade doidentifiernuma 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.timestamptem 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ãovolume. Comvolume, 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.