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 →