中文
Shopify knowledge · Guide

Usage Billing for a Shopify App: Define the Unit Before You Wire the Charge

Starting from a sync-style Shopify App, how to define a billable unit merchants can understand, separate previews and skips from real writes, and use an internal ledger, idempotent reporting, cycle quotas, and up-front quotes to avoid double or surprise charges.

The hard part of usage billing is rarely calling the charging API. It is defining what counts as one billable result. A vague "number of operations" mixes previews, skips, failed retries, and real writes together, leaving merchants unable to predict a bill and developers unable to reconcile one.

Shopify currently offers two billing paths. Pick one before designing the metering model, because they handle caps very differently.

Shopify App PricingBilling API (manual pricing)
PositionThe preferred approach for public appsStill supported, for existing apps and pricing models the other does not cover
Usage configurationDefine a usage meter under Pricing in the Partner Dashboard, report billing events through the App Events APIGraphQL mutations such as appSubscriptionCreate
Per-cycle capUsage caps are not currently supportedcappedAmount limits what a merchant is billed within a 30-day cycle

Shopify App Pricing has several hard constraints that shape the implementation directly: at most five active usage meters per plan, at most six pricing tiers per meter, usage charges must be billed monthly (they cannot be combined with yearly-only plans), and after a merchant uninstalls the app you have 24 hours to submit any remaining billing events — after that the billing period closes and new events are rejected. See Set up usage charges; for the cappedAmount behavior, see usage-based subscriptions with the Billing API.

The platform handles the subscription and the bill. Metering, cost estimation, and idempotent reporting stay with the app.

Define the unit first

A good unit satisfies four conditions:

  • The merchant can understand it before the run.
  • The app can compute it exactly after the run.
  • A retry does not produce a second valid unit.
  • It tracks the underlying cost reasonably well.

For cross-store sync, "each resource or field value successfully written to the target store" is a workable unit. You still have to decide each of these cases:

OutcomeBillableWhy
Diff preview onlyNoNothing in the target store changed
Skipped because already identicalNoNo new write was produced
Write failedNoThe merchant did not get the promised result
Write succeededYesA verifiable result exists in the target
Retried after a failure, then succeededOnceA result should only be confirmed once
Deliberately overwritten again by the userPer product ruleState it in the pricing description

Do not bill from a task engine's done count. Many engines mark "already identical" and "existing mapping found" as done, and done is not the same as a billable write having happened.

Estimate and settle from the same set

The pre-run quote should come from the same authoritative diff:

candidate resources
  → filter out identical, non-executable, and missing-dependency items
  → the projected write set
  → projected units and maximum cost

The final charge is then based on the set of writes that actually succeeded. An estimate is not the final bill, but both must classify by the same rules, or the user sees "estimated 20, charged 87" with no way to tell where the gap came from.

The confirmation screen should show at least:

  • Projected units for this run.
  • Remaining quota in the plan — under Shopify App Pricing this is maintained and enforced by the app, since the platform provides no usage cap.
  • Possible overage cost.
  • Whether failures and skips are billed.
  • Clear cancel and confirm actions.

Do not block a task with a notice bar that has no visible action. Confirming a charge is a decision the user makes, and it deserves a stable screen they can come back to.

The internal ledger is the source of truth

The app should write to its own usage ledger first, then send pending events to Shopify:

UsageLedger
- shopId
- billingCycleId
- operationId
- itemKey
- units
- status: pending | reported | failed
- providerRecordId
- createdAt

A workable uniqueness boundary is (operationId, itemKey, meter), or an idempotency key with equivalent semantics. When a job restarts, a request times out, or a worker retries, one business result must still produce exactly one valid ledger row.

The ledger separates "the business succeeded" from "Shopify accepted the report":

  1. The target write succeeds.
  2. Write the ledger row under the idempotency key.
  3. Report to Shopify asynchronously.
  4. Store the platform record ID or the failure reason.
  5. Re-report pending rows on a schedule and reconcile against Shopify's billing state.

That reporting window is not open-ended: under Shopify App Pricing, once a merchant uninstalls you have 24 hours to submit the remaining billing events. The catch-up job has to treat the uninstall event as a trigger and drain that store's pending rows immediately, rather than waiting for the next fixed-interval scan.

If you call the charging API at the end of the business request instead, a timeout leaves two bad outcomes: a write that was never billed, or a retry that billed twice.

Cycles, quotas, and trials

A quota is not a number that accumulates forever; it is bound to a billing cycle. Under Shopify App Pricing the platform will not cap anything for you, so overage protection is entirely the app's job. Under the Billing API, cappedAmount limits what is billed in the 30-day cycle, and raising it needs the merchant's approval; the app should still track the remaining balance locally so it can warn in advance.

For a long job that spans cycles, either lock the billing rules at the start or assign each item to the cycle it actually completed in. Either is defensible, as long as the rule is published in advance and applied consistently.

Trials need their own rules:

  • Whether the trial has a unit ceiling.
  • Whether exceeding it pauses the work or starts charging.
  • Whether converting to paid grants a fresh cycle quota.
  • How a job created during the trial but finished afterwards is handled.

"Free during the trial" answers none of these. It states a price, not a usage limit or which cycle the work belongs to.

Protecting cost

Free previews should not mean unlimited resource use. For previews that require heavy API querying, computation, and database access, add:

  • Separate rate-limit keys per capability, so a file listing and a cost quote do not share one cooldown.
  • A per-store concurrency limit.
  • A per-scan ceiling and pagination.
  • Queue capacity and timeouts.
  • Budget or anomaly alerts.

Present these as service protection, not as units quietly folded into the sync charge.

Reconciliation and acceptance

  • Retrying one successful item repeatedly leaves exactly one charge in both the ledger and Shopify.
  • Skips, failures, and previews produce no billing records.
  • A timed-out report can be safely re-sent.
  • Units for a job spanning cycles are assigned according to the published rule.
  • After an uninstall, that store's pending ledger is either drained within the 24-hour window or explicitly abandoned.
  • Plan upgrades, cancellation, and trial expiry move entitlements and ledger cycles together.
  • The merchant can see used, remaining, reset time, and the projected cost of this run.
  • The app can trace a business record to a platform charge, and work back from any bill line to the records behind it.

Shopify's billing capability answers "how this reaches the merchant's invoice". The metering model answers "why this amount is owed". Settle the second before you write the price list or integrate the API.