Control credit spend

Estimate a job's credit cost before it runs, enforce budgets per feature and tenant, read usage from headers and the Analytics API, and alert on burn rate.

  • 40 min
  • Advanced

The first three lessons made each call cheaper or rarer. This one makes spending visible and bounded. The failure it prevents is familiar: a new feature ships, a retry path misbehaves, one customer imports a large file, and the first sign is an out-of-credits 429 on your signup form three weeks before renewal.

Four habits prevent it: know what each call costs, estimate a job before it starts, budget by feature and tenant, and watch the burn rate rather than the balance.

Price every call

On APIVerve the cost of a successful call is fixed per API and shown on each API's reference page. It varies by more than an order of magnitude, so "a call" is not a unit you can budget in:

APICredits per call
Number to Words, Hash Generator, Query String Builder1
Country Lookup, Exchange Rate2
DNS Lookup6
IP Lookup, Phone Number Validator, Job Postings (per page)10
Email Validator, Text Summarizer, HTML to PDF, Job Search20
Website to Text25

Keep these in one config object that every estimator and ledger reads, and check it when you add an API. A cost table copied into three places is three places to forget.

Try it liveIP Lookup10 credits
GEThttps://api.apiverve.com/v1/iplookup
curl "https://api.apiverve.com/v1/iplookup?ip=8.8.8.8" \
  -H "x-api-key: YOUR_API_KEY"
Sign in or create a free account to run this with your own key.

10 credits for this answer. The data for an IP changes slowly, so caching it for a day turns a per-request cost into a per-day one.

The billing rules from Fundamentals still hold and drive the estimates below: a successful call is billed at the API's cost, a successful but empty answer is billed, and a failed call (validation error, rejected key, rate-limited or blocked call) is not. In a batch, only successful items bill, and billed reports how many.

Estimate before you run

A job's cost is arithmetic you can do before the first call:

estimate.mjs
import { buildQuery } from './query.mjs';

export const COST = { numbertowords: 1, countrylookup: 2, iplookup: 10, jobpostings: 10, emailvalidator: 20 };

export function estimateJob(api, paramsList, { pagesPerInput = 1, retryAllowance = 0.02, ratePerMinute = null } = {}) {
  const cost = COST[api];
  if (!cost) throw new Error(`No cost configured for ${api}`);
  const unique = new Set(paramsList.map((p) => buildQuery(p))).size;
  const calls = unique * pagesPerInput;
  const credits = Math.ceil(calls * cost * (1 + retryAllowance));
  const minutes = ratePerMinute ? Math.ceil(calls / ratePerMinute) : null;
  return { inputs: paramsList.length, unique, calls, credits, minutes };
}

retryAllowance is the honest part. Failed calls are free, but some retries are not, as the next section shows. Two percent is a starting point; replace it with the ratio you measure from your own reconciliations.

Then compare with the balance. The Fundamentals course used a 1-credit probe call to read the headers. There is a better source: GET https://api.apiverve.com/v1/analytics returns the account's summary and is never billed, on any plan. It also still answers when the account is out of credits, which is exactly when you need it.

preflight.mjs
import { estimateJob } from './estimate.mjs';

export async function usage() {
  const res = await fetch('https://api.apiverve.com/v1/analytics', {
    headers: { 'x-api-key': process.env.APIVERVE_API_KEY },
    signal: AbortSignal.timeout(10_000),
  });
  const body = await res.json().catch(() => null);
  if (!res.ok || body?.status !== 'ok') throw new Error(body?.error || `analytics HTTP ${res.status}`);
  return body.data;   // { summary, detailed, ... }
}

export async function preflight(api, paramsList, { reserve = 0.2, ...opts } = {}) {
  const { summary } = await usage();
  const perMinute = parseInt(summary.rateLimit, 10) || null;   // "60 requests/minute" or "unlimited"
  const est = estimateJob(api, paramsList, { ...opts, ratePerMinute: perMinute });
  const floor = Math.ceil(summary.tokensLimit * reserve);       // keep 20% for live traffic
  const fits = summary.tokensRemaining - est.credits >= floor;
  return { ...est, remaining: summary.tokensRemaining, floor, fits, renewalDate: summary.renewalDate };
}

Run preflight before every bulk job and refuse to start when fits is false. The reserve is a business decision: the job is the thing that should wait for renewal, never your production traffic. A refusal at minute zero with a clear number ("needs 412,000 credits, 380,000 available above the floor") is a planning conversation; a refusal at row 61,000 is an incident.

summary.rateLimit is a string, "60 requests/minute" or "unlimited", which is why it is parsed. The estimated minutes from lesson 3 come from the same call.

Check yourself

A job will validate 50,000 email rows, of which 31,000 are unique, at 20 credits each. retryAllowance is 0.02. Analytics reports tokensRemaining: 700000 and tokensLimit: 1000000, and preflight keeps a 20% floor. Does the job fit?

Run every example on your own key. Free plan, no card.

Scaling up?

Volume pricing, custom SLAs, and dedicated support for high-traffic teams.

Contact sales