← All field notes
Billing

Usage-based billing that only charges for success

Metering calls or resources is easy to count and easy to resent. Billing on the outcome the customer wanted is harder to define, costs the vendor money on every failure, and is the only model that survives a retry.

If your product's value is an outcome (a message delivered, a payment settled, a label printed), bill on the outcome. Not on the API call that asked for it, not on the resources it consumed on the way. Outcome billing is the only metering model that stays fair when the customer retries, and retries are the normal condition of every distributed system.

The cost of that choice lands on the vendor: every failure is work you did for free. This post is about making that trade deliberately, from the three things you can meter through to showing usage so the invoice is never a surprise. RocketPrint is a remote printing API and desktop Station for software platforms: your backend POSTs a print job, and a small app beside the printer prints it, with no inbound networking, no drivers for raw label formats, and a durable job record. Our print job billing is the worked example because we had to make each of these decisions and can defend them.

There are only three things you can meter

Every usage-based price is built on one of three units. The choice decides who carries the risk of failure.

Unit Examples What the customer pays for Who eats a failure
Calls API requests, messages submitted Asking Customer
Resources Seats, compute-seconds, GB stored, printers registered Capacity, used or not Customer
Outcomes Messages delivered, payments settled, print jobs completed The thing they wanted Vendor

Call metering is the easiest to implement, because the counter increments before you do any work. It also means a customer whose worker retried a timed-out request five times paid five times for one intent. They will notice.

Resource metering (seats, per-printer, per-device) is predictable and gives the vendor a quiet incentive to make de-registration hard. A customer with 40 registered printers and 3 in use pays for 40.

Outcome metering charges when the customer got what they asked for. It matches the mental model of the person approving the invoice: "we shipped 18,000 orders, we paid for 18,000 labels."

Outcome billing moves failure cost onto the vendor, and that is the point

When you bill on outcomes, you have made a promise: if it did not work, it did not cost anything. Every failed attempt is a request you authenticated, validated, stored, dispatched, and tracked, without revenue.

That is a real cost, and it is also the one trust signal a metered product can send that the customer can verify from their own logs. A customer who sees failed jobs in their list and no charge for them stops auditing your invoice.

The billable event must be defined so a retry cannot create a second one

The definition needs to be exact enough to put in a contract and to implement as a database constraint. Ours is:

A completed print job. One per business identity. Copies on a single job count once. Idempotent replays are never billed. Failed and canceled jobs are never billed.

Each clause closes a specific hole.

One per business identity. A caller sends Idempotency-Key: order-1042-shipping-label. Their worker times out and sends it again. The API returns the original job with the Idempotency-Replayed header instead of creating a second one. One job, one possible completion, one billable event. The billing system never de-duplicates, because the API already did. The mechanics are in how idempotency prevents duplicate labels.

Copies count once. A job with copies: 3 produces three pieces of paper and one completed print job. We could count three. We chose not to, because the customer made one decision, and because billing per copy invites people to write three jobs instead of one, which makes their integration worse for no reason.

Replays are never billed. This follows from the first clause, but it belongs on the pricing page separately because it is the thing a billing engineer will test on day one.

Failed and canceled are never billed. A job canceled with DELETE /v1/print-jobs/:id while still pending never became the thing the customer asked for. The same logic covers a job that hit its expiresInSeconds deadline before dispatch: it is expired, not completed.

The shape transfers. A messaging product bills on delivered messages, one per idempotency key, not on segments retried. A payments product bills on settled captures, not on authorization attempts. Find the one state that means "the customer got what they wanted," key it on the customer's identity for the intent, and count it exactly once.

Metering has to survive late and unknown outcomes

Outcomes for physical or delivered results arrive late, and sometimes never.

A job submitted at 09:00 may sit pending for hours because the computer beside the printer is off, then print at 14:00 when it comes back. The billable event is at 14:00, in whichever month contains 14:00. A meter that counts at submission has billed for something that may still expire.

A job can also end unknown: the Station was interrupted with an unconfirmed outcome. Something may have printed. We do not know, so we do not bill it. A later Station acknowledgment can settle the job to completed, and only then does it count. The bias is deliberate: when in doubt, the vendor loses the dollar.

The implementation that makes this safe is the same one that makes any event pipeline safe:

Rule Why
Meter from the terminal-state transition, not from the request Submission is not an outcome
Write the usage record in the same transaction as the state change A crash between the two is a lost or phantom charge
Key the usage record on the job id, with a unique constraint Redelivery of the transition event cannot double count
Never meter a non-terminal state printing can still become failed
Treat unknown as unbilled until it settles Doubt is the vendor's cost
Keep the job record after content is purged You need it to explain the invoice

The last rule has a concrete shape for us. Print content is deleted 7 days after a job reaches a terminal state, but the job record with its status, timestamps, failure reason, and usage stays, and it reports contentPurgedAt. An invoice line from six months ago can still be traced to a list of job ids with the timestamp each completed. The job status reference covers which states are terminal and what each proves.

Show usage all month so the invoice is never a surprise

A usage-based invoice that arrives as a number at the end of the month is a support ticket waiting to happen. Two things prevent it. The billed unit has to be something the customer can independently count: ours are completed print jobs and active customer workspaces, and GET /v1/print-jobs filtered by status is the same source we bill from. And the plan quotas have to be stated in those units and nothing else.

Plan Monthly price Active customer workspaces Completed print jobs
Launch $60 20 100,000
Orbit $500 200 500,000
Constellation $1,500 750 2,000,000

An active customer workspace is one with Station presence or at least one completed job in the month; a workspace provisioned for a prospect who never installed anything is not counted. Overage above quota is billed at a per-tier rate, and we never cut a customer off mid-shift. A warehouse that crosses its quota at 15:00 on a Friday keeps printing and the overage shows up on the invoice, because a hard limit on a physical operation punishes the customer's customers for our billing design. The full table and overage rates are on the pricing page.

Decide what is free and say so

Every metering model has boundaries where it would be easy to charge and you choose not to. List them, because the absence of a fee is a fact the buyer needs.

We do not charge per printer, per Station, per API key, or per user. Each would be simple to count and each would push customers toward worse integrations: keys shared across services, printers left unregistered, one login handed around a warehouse. Everything that scales with good hygiene is free.

Ordinary-use boundaries still exist. Per-key rate limits return 429 with a Retry-After header, and print bodies have a size limit, because a runaway loop is an operational problem, not a billing one. Those limits are in the API reference, and they are enforced by refusing the request, not by charging for it.

What this costs us, honestly

Three things.

Every failed, expired, and unknown job is work done for free. A customer with a misconfigured printer can generate thousands of failed jobs in a day and pay nothing. We accept that because the alternative is billing people for our inability to print, and because a customer with thousands of failures needs support, not an invoice.

completed means the OS print path accepted the print command. It is not proof that paper emerged. A printer that accepts the command and then jams produces a billed job and no label. We say this on the reliability page rather than hide it. That is the honest edge of outcome billing: we bill on the last thing we can observe, and we are explicit about where observation ends.

And the metering is more complicated than a counter. It has to be transactional with state changes, keyed for exactly-once accounting, tolerant of outcomes that land in a different month than the request, and reconstructable after content is purged. A per-call model has none of that cost. If you are building the same thing anyway, define the event first, build the meter around the terminal state, and write down what is free.

END / usage-based-billing-charge-only-for-success

More field notes