Usage-based billing for SaaS looks simple on a pricing page: customers pay for what they use. The operational reality is much messier when every customer request triggers paid third-party API calls, results arrive partially, networks time out, and a provider can charge you even when your user receives nothing.
A recent discussion in the r/SaaS community captured the problem well. The original poster was building an API-heavy product and moved to credits because its underlying costs vary by request. They discovered that charging a customer for 100 requested results when only 60 were actually returned felt indefensible, so they began reserving credits before work starts and deducting only for delivered output. That is the right instinct—but it is also the beginning of building a real billing system, not merely adding a balance field to a database.
This article breaks down how to design usage-based billing for SaaS products with volatile API costs. It covers the credit ledger, partial fulfillment, idempotency, failure policies, margin protection, customer communication, and the operational controls that keep one bug or retry storm from turning into an expensive incident.
Why usage-based billing for SaaS is harder than it appears
A fixed monthly plan turns a complicated cost model into a simple customer promise. A customer pays a predictable amount, and the business absorbs the variability in usage. That works when average usage is stable enough, gross margins are forgiving enough, or limits can be set without damaging the product experience.
API-heavy SaaS products often do not have that luxury. A search enrichment platform, AI workflow tool, lead-data product, image-generation app, transcription service, or research agent may incur a cost every time a user asks the software to do something. One customer may run a handful of lightweight jobs per month; another may submit thousands of high-cost requests in an afternoon.
Credits are attractive because they create a buffer between the customer-facing price and the provider-facing unit cost. Instead of exposing users to a confusing list of tokens, pages fetched, model calls, enrichment hits, proxy requests, or compute seconds, the product sells a simpler internal currency. That currency can represent value rather than raw infrastructure consumption.
But credits do not eliminate complexity. They relocate it into product design, accounting logic, and customer trust.
The r/SaaS post highlights four of the issues that separate a basic pay-per-use model from a durable one:
- A request can be only partially fulfilled.
- Retried jobs can create duplicate charges.
- Users can accidentally trigger expensive workloads.
- An upstream provider can bill the SaaS even when the user receives no usable result.
Each issue is both a technical question and a pricing-policy question. The code decides what happened. The policy decides who should pay for it.
The central principle: bill for delivered value, not attempted work
The cleanest customer-facing rule is straightforward: charge for the value that was successfully delivered according to the product’s published definition of completion.
That definition matters. In a data-enrichment product, the billable unit could be a valid enriched record. In an AI writing tool, it could be a completed generation above a minimum quality threshold. In a scraping product, it might be a successfully retrieved and parsed page. In an email-validation product, it might be a completed verification response rather than an attempted lookup.
The original Reddit poster’s approach—reserve credits for the maximum likely work, then settle against the number of results actually delivered—is a practical implementation of this principle. If a user asks for 100 results and receives 60 valid results, they pay for 60, not 100. The remaining reserved balance is released.
Why attempted-work billing creates avoidable conflict
Charging for all requested units is tempting because it maps neatly to the cost of launching the job. Yet it produces an immediate trust problem when customers can see the gap between what they asked for and what they received.
From the buyer’s perspective, a request for 100 contacts that returns 60 contacts is not 100 units of value. Even if the provider attempted 100 lookups, the product promise was about results, not effort. The customer usually cannot evaluate the vendor’s downstream API costs, retry behavior, or rate-limit problems—and should not have to.
A business can sometimes charge for attempted processing, but it must be explicit and appropriate to the category. For example, a transcription product might charge by uploaded audio minute because it has clearly processed those minutes. A compliance scan might charge per document analyzed even if no issue is found. The key distinction is whether the customer can reasonably understand the billable event and receive the core service promised.
Define what counts as a deliverable before writing billing code
Teams often start with a technical event such as “provider API returned HTTP 200.” That is rarely an adequate billable-event definition. A successful HTTP response can still contain empty data, malformed output, partial content, or a provider-side error embedded in the payload.
Instead, define a small set of business outcomes:
- Completed and billable: the user received a valid unit of promised value.
- Completed but free: work finished, but the output did not meet a minimum usable standard.
- Customer-caused rejection: invalid input, unsupported request, or a violation of published constraints.
- Platform or provider failure: an upstream dependency, timeout, internal bug, or infrastructure event prevented delivery.
- Ambiguous outcome: the system cannot yet tell whether the provider completed the task or whether a retry would duplicate it.
Those states create a common language for product, support, engineering, finance, and customers. They also prevent the worst outcome: silently treating uncertainty as a billable success.
Build a credit ledger, not a mutable balance
A credit balance alone is not a billing system. If the only record is that an account had 1,000 credits yesterday and has 840 today, you cannot reliably explain why 160 were removed, reverse an incorrect deduction, distinguish a duplicate job from a legitimate one, or defend a payment dispute.
The highest-value point from the community discussion was the recommendation to treat the reserve-then-deduct mechanism as a ledger. That is exactly right. A ledger is an append-only record of changes and their reasons. It makes balances explainable rather than mysterious.
Stripe’s usage-billing documentation similarly treats usage as a stream of metered events that is aggregated for billing, rather than as a single mutable counter. Stripe also recommends unique event identifiers for meter events, which is a useful model even if your product does not use Stripe for usage calculation. Its documentation notes that meter events are processed asynchronously, reinforcing a practical lesson for founders: usage accounting should be designed for delayed, retried, and out-of-order events rather than assuming every action settles instantly.
A practical ledger structure
A useful internal credit transaction record should include at least:
- A unique transaction ID.
- Customer, workspace, and project identifiers.
- A job ID and a client request ID.
- The transaction type: purchase, reserve, release, debit, refund, expiry, manual adjustment, or promotional grant.
- The credit amount, expressed as a signed value or a clear debit/credit field.
- The status: pending, posted, reversed, or failed.
- The billable-unit count and the unit definition used.
- Provider name, provider request ID, and upstream cost where available.
- Timestamps for request, reservation, execution, completion, and settlement.
- A reason code that support staff can understand without reading application logs.
Your current available balance should be a derived result of posted ledger entries plus any active reservations. That approach costs more to implement than updating a credits_remaining field, but it pays for itself when you need a customer-facing usage history, a refund workflow, or a reliable audit trail.
Separate authorization from settlement
The best mental model comes from payment cards. A card transaction often has an authorization first and a final settlement later. The same pattern works for credits:
- Estimate the maximum credits the job may consume.
- Confirm the customer has enough available balance.
- Place a temporary reservation.
- Execute the work.
- Count valid delivered units.
- Convert the appropriate portion of the reservation into a settled debit.
- Release the unused reservation.
Suppose a customer has 500 credits and starts a job that could return up to 100 results at one credit each. Reserve 100 credits, leaving 400 available for other work. If 63 valid results arrive, settle 63 credits and release 37. The final balance becomes 437.
This pattern prevents a customer from launching multiple concurrent jobs that collectively exceed their balance. It also avoids the unfairness of charging the maximum merely because the system had to reserve capacity before it knew the final outcome.
Expire reservations deliberately
Reservations can become stranded when workers crash, webhooks are lost, or a job gets stuck in a queue. Every reservation needs an expiry rule and a reconciliation process.
For example, a reservation might expire after 30 minutes for a synchronous request, four hours for a background export, or a longer documented period for a large batch job. An automated worker should inspect reservations approaching expiry, determine whether the underlying job is still active, and either extend the hold or release it.
Never rely entirely on timeouts, though. A long-running job may still succeed after a worker restart. Use job state, provider request IDs, and completion records to reconcile before releasing or recharging funds.
Partial results require unit-level accounting
Partial fulfillment is the scenario that most quickly reveals whether a usage model is fair. It is not enough to say “we charge only for successful results” if the system cannot identify which results are successful and why others were excluded.
For batches, store a result record—or at least a compact result event—for each requested unit. That record can include the item identifier, outcome, validation status, provider response ID, retry count, and billable flag. You may not need to retain the full third-party payload forever, but you do need enough evidence to reconstruct a charge.
Count valid output, not merely returned output
A provider can return something that is technically a result but commercially useless. Consider a lead-enrichment API that returns records with no email address, a scraping vendor that returns an HTML error page as page content, or an AI provider that returns an empty completion after a transient error.
Your billing logic should distinguish:
- Returned: the supplier sent a response.
- Valid: the response passed the product’s defined checks.
- Delivered: the customer could access the result.
- Billable: the result meets the stated threshold for charging.
In many products those four states will align. In edge cases, they will not. Designing for that difference protects the customer experience and gives the business better insight into which provider errors are eroding gross margin.
Establish minimum quality rules publicly
A vague promise such as “pay only for results” can create new disputes if a customer believes a thin or imperfect result should be free. Put practical rules in your terms, documentation, and in-product tooltips.
For example: “A credit is consumed when a requested domain returns a completed verification status,” or “A lead is billable when the returned record includes the fields selected in your workflow.” Avoid pretending that every output has identical value if the underlying product has meaningful quality variation.
The right answer is not to write a 20-page billing policy. It is to use a clear unit definition that a reasonable customer can check against their own export.
Provider failures: absorb first, recover second
The hardest question in the original post was what to do when the provider bills the SaaS for a failed request but the end user receives nothing. In most customer-facing SaaS categories, the default answer should be: do not charge the customer for a failure outside their control.
That is not charity. It is the cost of offering a dependable product promise. Customers purchased your product, not a pass-through contract with your vendors. If your provider is down, your retry logic is flawed, or your network connection fails after the upstream request is accepted, transferring that risk to the customer creates support burden and makes prepaid credits feel unsafe.
The top community response framed this as a tax for not frustrating users and suggested building an assumed failure allowance into credit pricing. That is a sensible starting point, provided the allowance is based on observed data rather than a guess that becomes permanent.
Use a failure taxonomy before deciding who pays
Not every failed request deserves the same treatment. A defensible policy generally separates failures into four buckets.
1. Customer-caused failures. These include malformed inputs, unsupported URLs, invalid file types, requests exceeding disclosed limits, or attempts to process content the product explicitly does not support. A product may reject these before making an upstream call and charge nothing. If an API call has already incurred a real cost, a small minimum charge can be reasonable—but only if that rule is prominent and the customer had a realistic way to avoid the error.
2. Product-caused failures. Bugs, queue failures, bad deployments, and internal outages should be free to the customer. Credit the attempt automatically where possible.
3. Provider-caused failures. Rate limits, service outages, malformed upstream responses, and provider-side internal errors should normally be free to the customer. Record the evidence and seek a vendor credit or refund afterward.
4. Ambiguous failures. Timeouts are the dangerous category. You may not know whether the supplier received the request and performed the chargeable work. If you retry blindly, you may be billed twice or produce duplicate customer results. If you charge immediately, you may charge for nothing.
For ambiguous cases, mark the job as pending reconciliation. Delay settlement, query provider status endpoints or logs if available, and retry only with a stable idempotency key or a provider-supported request identifier. When certainty is impossible, the customer-friendly default is usually to release the reservation and absorb the uncertainty.
Price a loss allowance into the unit economics
Do not hide provider failures in a miscellaneous operating-expense bucket. Calculate them explicitly.
A simple contribution-margin formula for one billable unit is:
contribution margin = customer revenue per unit - variable provider cost - payment fees - expected failure loss - support and infrastructure allocation
If a usable result costs $0.04 from suppliers, payment and platform costs add $0.01, and historical unrecoverable failed requests add $0.003 per successful result, your cost basis is not $0.04. It is at least $0.053 before support overhead and desired margin.
This is why credits should not be priced as a one-to-one translation of provider units. The product is providing orchestration, validation, a better interface, customer support, retries, monitoring, and risk absorption. Those are real sources of value—and real costs.
Idempotency is a billing feature, not an implementation detail
Duplicate charges rarely come from a malicious customer clicking twice. More often, they come from ordinary distributed-systems behavior: a browser retries after a slow response, a background worker restarts, a webhook is delivered again, or a timeout occurs after the provider processed the request but before your service received confirmation.
Stripe’s API documentation describes idempotency keys as a way to safely retry write operations without performing the same operation twice. That principle is universal. Every charge-affecting action in an API-cost SaaS should have a durable idempotency strategy.
Use stable IDs at each layer
A robust flow often requires more than one identifier:
- Client request ID: created by the browser or API caller for an intended action.
- Job ID: created by your application when the request becomes a unit of work.
- Ledger operation ID: created for the reservation and settlement actions.
- Provider idempotency key: sent to the upstream API when supported.
- Provider request ID: returned by the provider and stored for later reconciliation.
The important property is stable reuse. If a customer retries the same request because their connection dropped, your system should find the existing job rather than create another job and another credit reservation. If your worker retries the upstream call, it should use the provider’s supported idempotency mechanism where available.
Make settlement idempotent too
Teams sometimes protect the external API request but overlook internal billing. A worker can successfully settle a debit, crash before marking the job complete, then restart and attempt another debit. The ledger must reject a second settlement for the same job and billing period.
Use a unique database constraint or transactional lock around the business key that represents the charge. For example, one settled debit may be permitted per (workspace_id, job_id, result_item_id, billing_version) combination. The exact schema will vary, but “we will check in code first” is not enough under concurrency.
Treat retries as product behavior
Retries improve completion rates, but every retry changes the cost profile. Establish retry policies per error category:
- Do not retry deterministic validation errors.
- Retry transient network and 5xx failures with exponential backoff and jitter.
- Respect provider
Retry-Afterheaders and rate-limit signals. - Cap retries by both count and maximum economic cost.
- Stop automatically when the remaining expected value is lower than the expected retry cost.
That final rule is underused. A customer who paid one credit for a low-value enrichment should not trigger ten expensive retries because the system optimizes solely for completion percentage. Reliability needs an economic boundary.
Protect the business from runaway API spend
The Reddit poster also added daily spending limits, which is one of the most important safety controls in an API-dependent product. Usage limits protect the business from customer mistakes, abusive automation, compromised accounts, bugs, and provider pricing surprises.
Cloud platforms make the same point at infrastructure scale. Google Cloud supports budgets and alerts, with some eligible services able to use spend-cap budgets that automatically pause qualifying usage when a cap is reached. AWS offers Cost Anomaly Detection, but AWS documents that anomaly signals can be delayed by up to 24 hours because they rely on Cost Explorer data. The lesson for SaaS founders is clear: provider-level alerting is useful, but it cannot be your first line of defense against a live runaway workload.
Build limits at several levels
A single “monthly customer limit” is too blunt. Use layered guardrails:
- Per-request maximum: prevent a user from asking for an unexpectedly huge batch.
- Per-job budget: stop a workflow once it has consumed its approved allocation.
- Daily workspace limit: cap accidental bursts and compromised API keys.
- Monthly account limit: preserve the plan’s economics and encourage top-ups or upgrades.
- Provider-specific circuit breaker: pause traffic when a supplier has abnormal errors, latency, or unit cost.
- Global kill switch: allow operations staff to stop a costly feature instantly.
Use soft limits and hard limits differently. A soft limit might trigger an email, in-app warning, or approval step at 80% of a daily budget. A hard limit rejects or queues new work at 100%. For expensive operations, require an explicit confirmation that shows the maximum estimated credit cost before execution.
Monitor margin, not just usage
A dashboard that says customers used 10 million credits is not enough. You need to see whether the credits consumed were profitable.
Track at least these metrics by customer segment, product feature, and provider:
- Revenue per delivered unit.
- Upstream cost per delivered unit.
- Failure rate before and after retries.
- Cost of failed attempts that customers were not charged for.
- Gross margin by customer and cohort.
- Reservation-to-settlement ratio.
- Refund and manual-credit rate.
- Provider latency, errors, and rate-limit events.
A customer with high usage can be excellent or disastrous depending on their workflow. If their requests have unusually low completion rates, require expensive provider routes, or trigger disproportionate support work, a generic credit price may be concealing negative margin.
Make credits understandable enough to earn trust
Credits simplify back-end pricing only if they do not create front-end confusion. Customers dislike credits when the exchange rate feels arbitrary, changes unpredictably, or makes it hard to forecast spend.
The solution is not necessarily to expose every third-party API cost. It is to explain what customers can control: what actions consume credits, how many credits those actions usually require, what happens with partial results, and when credits are returned.
A transparent customer experience should include
- A live available-credit balance and reserved-credit balance.
- A pre-run estimate for requests with variable scope.
- A job history showing requested units, delivered units, credits reserved, credits charged, and credits released.
- Clear labels for refunded or automatically reversed charges.
- Usage alerts before the customer hits a hard cap.
- Downloadable usage records for finance teams and agencies.
If your product sends email, notifications are especially valuable because usage limits are only helpful when people see them in time. Founders building these controls should think through the cost and deliverability of transactional notifications alongside the product economics; compare the practical transactional email pricing before treating alerts as an afterthought.
Good explanations also reduce support workload. “Job completed: 60 of 100 records found. You were charged 60 credits; 40 reserved credits were released” is dramatically better than a balance decrement with no context.
Avoid manipulative credit design
Prepaid credits can be legitimate, but they can also become a dark pattern when purchased amounts obscure real prices or expiration policies are buried. The more expensive and business-critical the product, the more important it is to provide a dollar-equivalent reference and clear purchasing terms.
Be cautious with expiry. Credits that expire can help with revenue recognition, promotions, or supplier commitments, but they also create customer resentment when users perceive that purchased value disappeared. If expiry is necessary, disclose it during purchase, send reminders, and make reasonable exceptions for good customers.
Choose the right pricing model for the risk profile
Credits are not the only answer to variable API costs. The best model depends on how predictable underlying costs are, how easily users understand the unit, and how much downside risk a single customer can create.
Credits
Credits work well when several expensive actions need a common currency or when the raw cost of a feature varies by supplier, model, geography, or complexity. They are flexible and can support prepaid cash flow, but they demand strong observability and clear communication.
Pure pay-as-you-go
Direct metered billing is easiest to understand when there is one obvious unit: emails sent, gigabytes stored, minutes transcribed, or documents processed. It can reduce unused-credit complaints but may create surprise bills. Usage-based billing vendors such as Stripe support recording and aggregating metered events, but your application still needs to decide what counts as legitimate usage before reporting it.
Subscription with included usage and overages
This is often the most founder-friendly compromise. Customers receive predictable monthly capacity, while heavy users pay for extra consumption. The included allowance cushions low-usage customers; overages protect the business when usage increases.
It also lets you keep the customer’s main decision simple: select a plan based on expected scale. If the product has a mostly predictable baseline with occasional spikes, this model can produce less pricing friction than standalone credit packs.
Cost-plus or enterprise commit contracts
For customers with highly variable, high-volume workloads, a fixed credit table may be too risky. Enterprise agreements can include a minimum commitment, negotiated rate card, pre-approved caps, and pass-through terms for unusually costly supplier services.
Do not use cost-plus as an excuse to export every operational inefficiency to a buyer. But when a customer requests a custom pipeline with unusual providers, data regions, or service-level requirements, transparent custom economics can be more honest than pretending a generic self-serve plan fits.
A rollout plan for founders who already have a basic credit system
You do not need a perfect financial platform before launching. You do need enough control to avoid charging customers incorrectly or losing track of real cost.
Phase one: make charges explainable
Start by recording every purchase, reservation, debit, release, refund, and manual adjustment in an append-only ledger. Add job IDs, result counts, and provider request IDs. Build a support view that can answer, “Why was this customer charged?” in less than two minutes.
Phase two: make outcomes fair
Implement reservation and settlement. Define billable completion criteria. Release unused reservations automatically. Do not charge for platform or provider failures while you are still learning the failure modes.
Phase three: make retries safe
Add idempotency keys across client requests, internal jobs, settlements, and provider calls. Create reconciliation workflows for timeouts and ambiguous results. Add tests that intentionally simulate duplicated messages, worker restarts, and delayed responses.
Phase four: make spend controllable
Add user-visible estimates, daily caps, per-job maximums, alerts, and kill switches. Monitor provider cost and gross margin at the feature and customer level. Review outliers weekly until patterns are stable.
Phase five: turn operational data into pricing decisions
After several billing cycles, analyze actual delivered-unit cost distributions rather than averages alone. Look at the 50th percentile, 90th percentile, and worst-case workloads. If the top 5% of jobs consume most of the margin, introduce tiers, complexity multipliers, or product limits designed around the real cost drivers.
The bigger lesson: usage pricing is a promise about risk allocation
The most important decision is not whether to use credits, dollars, tokens, or units. It is deciding which party bears the uncertainty created by third-party dependencies.
Customers will accept paying for measurable value. They are much less willing to pay for the possibility that a provider was slow, an API returned an error, a worker retried a request, or a product bug caused duplicate work. A SaaS business that absorbs those operational failures is making a deliberate commercial promise: the customer pays for outcomes, while the company manages the machinery required to create them.
That promise must be reflected in price. If provider failures, retries, payment fees, support, and monitoring make the model unprofitable, the answer is not to quietly charge users for unsuccessful jobs. It is to change the credit exchange rate, add minimums where customer behavior truly creates cost, improve supplier terms, tighten limits, or move high-variance users to a pricing model that matches their workload.
The r/SaaS poster is already on the right path. Reserving first, charging only for delivered results, preventing duplicate charges, and setting daily limits are not edge-case refinements. They are the foundational controls that make usage-based billing for SaaS feel fair to customers and survivable for the business.
FAQ
Should SaaS companies charge credits for failed API requests?
Usually not when the failure is caused by your product, network, or third-party provider and the customer received no usable result. Treat unrecoverable provider charges as a cost of service, then include a measured allowance for those losses in your pricing.
How should a credit system handle partial results?
Reserve the maximum expected credits before the job runs, then settle only for valid delivered units. Release the unused portion of the reservation and show the customer the requested, delivered, charged, and released totals.
What is the difference between a credit balance and a credit ledger?
A balance is a number. A ledger is the transaction history explaining how that number changed. A proper ledger records purchases, reservations, debits, releases, refunds, and adjustments with IDs, timestamps, and reasons.
How do you prevent duplicate usage charges?
Use idempotency keys for customer requests, background jobs, provider API calls, and internal settlement events. Enforce uniqueness in the database so a repeated worker message cannot settle the same result twice.
Are daily spending caps enough to prevent runaway API costs?
No. Daily caps are important, but combine them with per-request limits, per-job budgets, workspace limits, provider circuit breakers, anomaly monitoring, and an emergency global kill switch. Provider billing alerts can be delayed, so application-level controls must act first.