HomeServicesPortfolioAboutContactBlogCareers
Book a call
AI Engineering

Usage-Based Billing with Orb

October 2026 · Leo Karpov

The invoice is only as good as the events behind it

Usage-based billing with Orb moves the hard part of pricing out of the billing tool and into your product. Orb turns usage events into invoices, but it can only bill what it receives: if an event is lost, sent twice, sent late, or sent with the wrong customer, the invoice is wrong and the customer is the one who notices.

This article is about the part on your side of the line: getting usage from your product into Orb reliably, and correcting it safely when something goes wrong.

What Orb expects from an event

Events go to Orb's ingest endpoint in batches. Each event carries:

  • event_name, the kind of usage (for example an API call or a processed document);
  • timestamp, in UTC, when the usage happened;
  • properties, the details your billable metrics will filter and aggregate on;
  • idempotency_key, which Orb uses to ingest each event exactly once;
  • the customer, either Orb's customer_id or your own external_customer_id.

Using external_customer_id with your own account id keeps your code from having to look up Orb's ids on the hot path. Timestamps too far in the future are rejected, which is a useful guard against clock bugs on your servers.

Idempotency keys: generate once, store, reuse

Orb guarantees that only one event with a given idempotency key is ingested, which makes retries safe, but only if a retry sends the same key. The mistake to avoid is generating the key at send time: a retry then gets a new key, and the usage is billed twice.

Generate the key when the usage is recorded in your system (a UUID is fine), store it with the usage record, and send that stored key on every attempt. Orb's documentation also spells out how to treat errors: on a 4xx, fix the rejected events and resend them with their original keys; on a 5xx, retry the whole request.

Send events through an outbox

Calling Orb directly from the code path that performs the billable action couples your product's uptime to Orb's. The pattern we prefer is an outbox:

  1. In the same database transaction as the billable action, write a usage row: event name, timestamp, properties, customer, idempotency key, status pending.
  2. A background worker (Celery in our stack) picks up pending rows in batches and sends them to the ingest endpoint.
  3. Successful rows are marked sent; failures are retried with backoff; rows Orb rejects are marked for investigation with the error attached.

The outbox gives you three things a direct call cannot: usage is already stored on your side when Orb is slow or unavailable, sending is batched instead of one request per action, and you have a local record to reconcile against.

The grace period is a deadline

Orb accepts events for a limited time after their timestamp. By default that grace period is 12 hours, configurable per account, and older events are rejected. Pending invoices can still change during that window after a billing period ends, and are finalised once it passes.

In practice this means the outbox worker must keep up, and an outage on your side longer than the grace period turns into usage that can no longer be sent the normal way. Alert on the age of the oldest pending row, not only on errors, and set the threshold well inside the grace period.

Correcting mistakes without losing data

Usage data will sometimes be wrong: a bug double-counted, a property had the wrong value, events went to the wrong customer. Orb never overwrites or deletes ingested usage; instead it offers explicit corrections, and the originals are archived:

  • Amend an individual event, identified by its idempotency key, changing its properties (but not its timestamp or customer).
  • Deprecate an event so it no longer counts toward billing.
  • Backfill a time range, which replaces all events in that exact range with a corrected set.

Two cautions from Orb's own documentation. Idempotency is not guaranteed across normal ingestion and backfills, so do not run both for the same range at the same time. And for customers billed against prepaid credits, Orb advises against backfilling periods before the current one, because already-issued invoices cannot reflect the effect on credit deductions. Corrections to closed periods are usually a conversation with the customer and a credit, not a silent re-bill.

Metrics live in Orb, raw facts live in events

Billable metrics in Orb are queries over raw events: filters on properties combined with an aggregation such as a count or a sum. That suggests a design rule for your events: send raw facts (what happened, how much, for which resource), not pre-computed billing amounts. When pricing changes, you change the metric in Orb instead of changing what your product emits and re-sending history.

Webhooks back from Orb

Orb notifies your system about invoices and subscriptions, for example invoice.issued, invoice.payment_succeeded, invoice.payment_failed, and subscription events such as subscription.plan_changed. Delivery is at least once and Orb expects a 2xx within five seconds, so the usual rules apply: verify the X-Orb-Signature header (an HMAC over the timestamp and raw body with your endpoint secret), check X-Orb-Timestamp to reject replays, store the event, acknowledge, and process in a worker that deduplicates on the event id.

Where this stops being right

  • Low, predictable usage. If usage barely varies between customers, tiered flat plans are simpler to run and easier for customers to understand than metered billing.
  • Usage that must be priced in real time. If a customer needs to see an exact running bill to the cent as they work, you will be computing it on your side anyway; the billing platform confirms the number later.
  • Very high event volumes. At large scale, aggregate on your side before sending (for example per minute per customer) instead of sending every raw action, and accept that you have moved some billing logic into your own code.

FAQ

Should idempotency keys be random UUIDs? A UUID is fine, as long as it is generated once when the usage is recorded and reused on every retry, not generated at send time.

What happens to events that arrive after the grace period? They are rejected by normal ingestion. Keep the outbox worker well ahead of the grace period and alert on the age of unsent usage.

How do we fix usage that was already billed? Use amendments or deprecation for individual events and backfills for ranges, within the limits Orb documents. For closed periods, a credit to the customer is often cleaner than re-billing.

Usage-based pricing is mostly a data pipeline problem, which is why we treat it as both billing and integration work: see payment and subscription integration and, for the outbox and queue side, API and SDK integration.

See it in production
AI integration services → Portfolio → Payment and subscription integration →
← More on AI Engineering All articles