Cost kinds and grains
In productionShopify keeps one unit cost per variant and no history. Offer Suite keeps the cost of goods beside every cost Shopify has no field for (3PL fees, labels, inserts, packaging, freight, payment processing), each under a kind with a grain. The grain stops a cost being counted twice: a per-parcel kind on a three-pack is counted once, a per-unit kind three times.
Until you add your own amount for a kind, the profile's default applies, in forecast and realized profit alike. Freight and shipping boxes have no estimate yet: they count only once your agent posts an order's actual cost.
| Cost kind | Key | Counted | Default | Rules |
|---|---|---|---|---|
| Cost of goods | unit_cost | Per unit | None | Every order |
| 3PL base | 3pl_base | Per order | $1.18 | Every order |
| Extra pick | 3pl_extra_pick | Per unit | $0.20 | Each unit after the first in an order |
| Shipping label | 3pl_label | Per order | $0.10 | Every order |
| Insert card | 3pl_insert | Per order | $0.05 | First orders only |
| Freight | freight | Per parcel | None | No estimate yet: counted only from actual costs your agent posts |
| Shipping box | packaging_box | Per parcel | None | No estimate yet: counted only from actual costs your agent postsNot charged when the unit cost already includes packaging |
| Poly bag | packaging_plastic | Per unit | None | Not charged when the unit cost already includes packaging |
| Payment processing | payment_processing | Percent of revenue with shipping, after discounts, before tax | 2.9% | Replaced by the order's actual Shopify Payments fee once Shopify reports it (partly available) |
| Processing per order | payment_processing_fixed | Per order | $0.30 | Replaced by the order's actual Shopify Payments fee once Shopify reports it (partly available) |
Dated rows, voids, lookup
In productionA cost is a row with an amount and the date it takes effect, which can be in the past. Rows are appended, never edited: a correction is a new row for the same date, and the ledger keeps both, each with when it was recorded.
| Targets | A row costs one item (a SKU), a product, an offer or the whole workspace. |
|---|---|
| Lookup | An order line's cost falls back from its item, to the item's kit components, to the product, to the workspace. |
| Offer rows | An offer-level row is per order, per parcel or a percent, never per unit. |
| Voids | A void is final. A read as of an earlier time still sees the voided row, because it stood then. |
| As of | The API and MCP read the ledger and the effective costs as they were recorded at any past time (asOf) and as they applied on any date (at). |
| CSV import | All or nothing: one bad row refuses the file, unless the import is sent with partial=1. |
| Scope and base | Through the API and MCP, a kind can be limited to every, first, renewal, one-time or subscription orders, and a percent kind can take revenue with shipping, product revenue or unit cost as its base. |
The dashboard's Costs page adds rows, voids them, imports a CSV and seeds from Shopify.
Restatements and two views
In productionEvery write that moves a past cost (a row, a void, a CSV import, a Shopify seed, a kind change or a variant re-mapped to another item) records a restatement and recosts the affected past orders in the current view. The as-recorded view keeps the costs each order was booked with. Every realized read in the API, MCP and Sidekick takes the view to use.
Cost ledger
Amounts by kind and product over valid time; corrections and backdated rows marked
| Kind · target | Amount | From | To | Recorded | Note |
|---|---|---|---|---|---|
| Cost of goods · Single Bag | $12.00 | May 20 | today | Jul 10 | Backdated 51 days |
| Cost of goods · Starter Kit | $17.50 | May 20 | today | Jun 2 | Corrects $17.00 (May 20) |
| Cost of goods · Ritual Set | $20.00 | May 20 | Sep 13 | May 20 | – |
| Cost of goods · Ritual Set | $22.50 | Sep 13 | today | Sep 14 | Backdated 1 days |
| Packaging · Starter Kit | $0.60 | Jun 1 | – | Jun 1 | Voided Jun 3: Entered on the wrong SKU |
| Packaging · Starter Kit | $0.50 | Jun 1 | today | Jun 3 | Backdated 2 days |
| Gift fulfilment · all SKUs | $0.90 | May 20 | Jul 1 | May 18 | – |
| 3PL base · all SKUs | $1.18 | the epoch | today | May 18 | – |
Each restatement reports what triggered it, its scope (the items, products, offers or kinds it touches, and the date it reaches back to), how many orders it recosts, its status and the change in the workspace's realized profit since that date. The change is one figure for the whole job, not a breakdown per order.
| Restatement | From | Orders recosted | Status | Profit change |
|---|---|---|---|---|
| Shipping boxShipping box made required | All dates | 0 of 12,410 | Queued | Pending |
| Cost of goodsCSV import · 42 rows | Aug 1 | 1,900 of 3,120 | Running | Pending |
| Cost of goodsRitual set raised · $20.00 → $22.50 from Sep 13 | Sep 13 | 9 | Completed | −$22.50 |
| All kindsManual restatement from Jun 1 | Jun 1 | 300 of 8,900 | FailedClickHouse insert timed out on batch 4 | None |
| Cost of goodsSingle bag backdated · $12.00 from May 20; was missing | May 20 | 700 | Completed | −$8,400.00 |
| PackagingStarter kit voided · $0.60 entered on the wrong SKU; $0.50 from Jun 1 | Jun 1 | 40 | Completed | +$4.00 |
| Cost of goodsStarter kit corrected · $17.00 → $17.50 from May 20 | May 20 | 260 | Completed | −$130.00 |
A restatement that fails stays listed with its error and can be run again. The realized mirror is revisioned against the orders it copies, and a sweep every 5 minutes re-mirrors any order left behind and resends any restatement that was not sent.
Items, kits and catalog health
In production- Costs attach to an item, the physical SKU, so one row covers every variant that maps to it. A variant can be re-pointed to another item through the API or MCP, and its past orders are restated.
- A kit is an item with a dated bill of materials, nesting up to 8 levels and refusing cycles. For a per-unit kind it has no row of its own for, a kit costs the sum of its components at the order's date; a recipe set from a date never recosts orders placed before that date.
- Shopify fixed-bundle orders are matched to a kit only on an exact match: the same parts, a whole multiple of one kit. Anything else counts as its parts, never a guess.
GET /items/healthruns eight checks, each listing its offenders, among them variants with no SKU, SKUs whose variants had different Shopify costs, reused SKUs, items or kit parts with no cost, unmapped order lines, orders missing a required cost kind and recent mapping changes.- The same report gives the share of recent realized revenue on fully costed orders. The dashboard's Costs page shows that share and the uncosted items.
From Shopify
In production- One call seeds a cost-of-goods row per item from Shopify's unit costs, only where the amount changed, and can backdate them. Variants with no cost are skipped, not zeroed. When variants sharing a SKU carry different costs, nothing is written for that item and the conflict is reported, never guessed.
- Shopify overwrites unit costs and keeps no history, so each change it reports (a unit cost, a SKU, a new unmapped variant) becomes a dated proposal in an inbox you accept or ignore through the API, MCP or the dashboard's Costs page. Accepting appends a ledger row and restates.
- Auto-accept takes plain unit-cost changes only: never a cleared cost, a cost that conflicts with another variant of the item, or a SKU or new-variant proposal.
Actual costs replace estimates
In productionYour agent, or a script relaying your 3PL's or carrier's invoices, posts each order's actual shipping and fulfilment costs to POST /orders/actual-costs (the MCP tool record_actual_costs), up to 500 rows a call. Each actual replaces that kind's estimate in realized profit, in both views.
- Rows are append-only and idempotent by
sourceRef: a credit is a negative row under its own reference, and a conflicting resend is refused. - Per-parcel kinds are charged once per non-cancelled Shopify fulfilment, at least once per order.
GET /orders/costs(get_order_costs) returns one order's estimate, actual and variance by kind, as the API computes them.
Payment processing
Partly availableThe actual-fee read has not yet run on a live Shopify Payments order.
Payment processing is a cost in both forecast and realized profit. The estimate is 2.9% of revenue as charged (lines after discounts, plus shipping, before tax) plus $0.30 per order, and either amount can be changed through the costs API.
On Shopify Payments orders that arrive by webhook, Offer Suite reads the actual fee from Shopify for up to about 12 hours; once it is known, it replaces the estimate on that order in realized profit. Other gateways, imported orders and orders whose fee never appears keep the estimate, and the forecast always uses it.
Lines added after the sale
On the dev API, not in production yetOnce an order is paid, a line that a post-purchase upsell or any other order edit adds is stored as its own line of the order. It is costed from the ledger at the order's date, counted in that order's offer and option contribution in both views, and its refunds net against it.
A quantity change to a line the order was placed with stays an uncosted adjustment, shown apart from contribution. Upsell profit is not yet reported apart from the front-end sale.
Settings
In productiondefaultCostProfile | The cost profile for orders no offer claims. Today dtc-us is the only one; null leaves those orders uncosted. |
|---|---|
autoAcceptShopifyCosts | Accepts Shopify unit-cost proposals automatically, but only while Shopify is the unit cost's canonical source. |
unitCostIncludes | What the Shopify unit cost already includes (packaging, inbound freight), so those kinds are not charged twice. Changing it restates the current view. |
Set through the API (GET and PATCH /tenant) or the MCP tools get_tenant and update_tenant.
What it does not do yet
- No built-in 3PL or carrier connector: actual costs arrive from your agent or a script.
- The dashboard has no as-of view and no controls for a kind's order scope, percent base or unit-cost inclusions; those are API and MCP only.
- The dashboard's Shopify seed shows only counts, not the conflicts it refused, and the dashboard and App Home have no settings page.
- Variance between estimated and actual costs is shown per order, not summed across offers.
Questions
What happens to last month's profit when I fix a cost?
The correction is a new dated row. Offer Suite records a restatement, recosts every order it reaches in the current view and reports how much realized profit moved. The as-recorded view still shows each order with the costs it was booked with, so both figures stay available.
How do I know a profit figure is fully costed?
The items health report lists items, kit parts and orders without a required cost, and gives the share of recent realized revenue on fully costed orders. The dashboard's Costs page shows the same share. Fully costed counts the profile's default amounts (3PL fees, label, insert, payment processing) for any kind you have not set yourself, so set your own to make the figure yours.
Can it read my Shopify unit costs?
Yes. A seed writes one cost-of-goods row per item from Shopify's unit costs, and every later change Shopify reports arrives as a dated proposal you accept or ignore. Conflicting costs on one SKU are reported, never averaged.
Are payment fees an estimate?
On Shopify Payments orders that arrive by webhook, the actual fee replaces the estimate once Shopify reports it. Otherwise the fee is estimated at 2.9% plus $0.30 per order, which you can change. The actual-fee read has not yet run on a live Shopify Payments order.