Getting StartedEcommerce Spec

Ecommerce Spec

One canonical shape for every ecommerce event — GA4-style names, an items[] array, value and currency — so Meta, Google Ads, GA4 and OpenAI activation is entering an id rather than mapping fields.

Why there is a spec

Ad platforms do not fail loudly. Meta accepts a Purchase with no value and reports a conversion worth nothing. Google accepts a conversion whose cart_data has no items and reports it with no Shopping signal. Both look healthy in a delivery log.

They also disagree about names for the same facts. Meta wants contents[].item_price, Google wants items[].unitPrice, GA4 wants items[].price. Every one of those means the unit price. So the only thing that makes activation cheap is a single input shape they can all be projected from.

That shape is the Mythic ecommerce spec. Send it, and connecting a platform is entering your pixel id — the browser tag and the server destination are both already mapped.

On Shopify this is automatic. The Web Pixel and the order webhook both emit this spec, so the only thing left is the platform id. This page is for everyone sending events through the SDK or the server-side ingestion API directly.

Event names

Snake_case, matching GA4. Not a style preference: the managed Meta pixel's default mapping, the Google Ads and GA4 destinations, and the OpenAI event taxonomy all key on these names already.

EventWhenCarries value
view_item_listA collection, category or recommendation list was seenNo
view_itemOne product pageYes
add_to_cartA line was addedYes
remove_from_cartA line was removedYes
view_cartThe cart was seenYes
begin_checkoutCheckout startedYes
checkout_step_completedA checkout step with no GA4 equivalent (contact info, address)Yes
add_shipping_infoShipping selectedYes
add_payment_infoPayment enteredYes
purchaseThe order was placedYes
refundThe order was refundedYes
order_cancelledThe order was cancelledYes
searchA search was runNo

view_item_list and search deliberately carry no value. Their items[] is a catalogue, not a basket — deriving a value from it would report the sum of everything on the page as revenue. A 24-product collection view would reach Meta as a conversion worth the whole collection.

Event properties

items and item_count are always present, even when empty. Everything else is omitted when unknown rather than sent as null.

PropertyTypeMeaning
itemsarrayLine items. Always present. See the item shape.
item_countnumberSum of quantity across items. Present because an array cannot be aggregated in a trend — this can.
valuenumberThe monetary value of this event. See what value means.
currencystringISO 4217, upper case.
transaction_idstringThe order id. Google's dedup key and Meta's order_id. Only on purchase, refund and order_cancelled.
checkout_idstringThe checkout token. Never the order id.
cart_idstringThe cart id.
affiliationstringThe store the transaction happened in.
couponstringOrder-level discount code(s).
subtotalnumberItem total before shipping and tax.
shippingnumberShipping cost.
taxnumberTotal tax.
discountnumberTotal discount.
search_termstringThe query, on search. A plain string — never buried in a filters array, because no destination mapping can reach into one.
list_id / list_namestringThe list, on view_item_list.
payment_methodstringPayment gateway used. Only known once payment is taken, so it appears on purchase.
shipping_methodstringShipping tier chosen.
checkout_stepnumber1-based step index within checkout.
new_customerbooleanFirst-ever order for this customer. Absent when the producer cannot tell — never guessed. purchase only.
customer_order_numbernumber1-based lifetime order count including this order, so a first order is 1. purchase only.
customer_lifetime_valuenumberLifetime spend including this order, in currency. purchase only.
purchase_typestringone_time or subscription. A mixed cart is subscription. purchase only.

Customer lifecycle properties

The last four are how you segment new versus returning buyers, bid differently on a second order than a fifth, and separate subscription revenue from one-off revenue.

Two things to know before you build on them.

Absent means unknown, not false. new_customer is only sent when the producer actually knows. A guest checkout has no customer record, so the property is absent rather than false — because false asserts "this is a returning buyer", and a value-based bidding strategy will act on that assertion.

Availability depends on the producer. For Shopify specifically:

propertyWeb Pixel (browser)Order webhook (server)
new_customeryes — isFirstOrderyes
customer_order_numberlogged-in customers onlyyes
customer_lifetime_valueno sourceyes
purchase_typeyesyes

One sale reaches Mythic from both producers and purchase dedupe keeps one copy — usually the browser's, because it arrives first. Measured on a live order, that surviving copy carried new_customer and purchase_type and neither counter.

So the counters are also written to the person profile as orders_count and total_spent, from a path that is never deduped. Two consequences worth knowing:

  • Filtering and segmenting on the profile always works, whichever purchase copy survived.
  • Activation works too: when the purchase event lacks them, Mythic recovers them from the profile before sending to Meta and GA4. A value the producer did send always wins — the profile is only a fallback.

These reach GA4 automatically as event parameters — it forwards every non-$-prefixed property. Meta needs them listed in custom_data_mapping, which the managed integration and migration 98 do for you. If you configured a Meta destination by hand, add them there.

transaction_id and checkout_id are different things

checkout_id is the checkout session. transaction_id is the order, and it does not exist until the order does — so begin_checkout has a checkout_id and no transaction_id.

Keeping them apart is what lets transaction_id be a dedup key. It is the value Google matches browser and server conversions on, and both halves of a Mythic purchase send the same one.

The item shape

Key names are GA4's, so the GA4 projection is nearly an identity and every other platform is a rename.

PropertyTypeMeaning
item_idstringProduct id.
item_variant_idstringVariant id, when it differs from the product.
skustringSKU. Preferred as the catalogue key when projecting to Meta.
item_namestringProduct name.
item_brandstringBrand / vendor.
item_categorystringCategory / product type.
item_variantstringVariant label ("Large / Blue"), not the variant id.
pricenumberUnit price. Never a line total.
quantitynumberUnits on this line. A missing quantity is 1, never 0.
discountnumberPer-unit discount, GA4 semantics.
couponstringLine-level discount code.
indexnumber1-based position in the list this item was seen in.
item_url / item_image_urlstringProduct page and image, as absolute https:// URLs.

price is the unit price on every event. This is the single rule most worth getting right: Meta's item_price and Google's unitPrice both mean unit, and a line total sent here multiplies the reported item value by the quantity with nothing rejecting it. If your platform gives you a line total, divide it by quantity before sending.

What value means per event

Eventvalue
view_itemUnit price of the product
add_to_cart / remove_from_cartUnit price × quantity of the line, not the cart
view_cartCart total
begin_checkout, checkout_step_completed, add_shipping_info, add_payment_infoCheckout total
purchaseThe total charged, including shipping and tax. subtotal is the pre-shipping figure.
refundAmount refunded, positive
order_cancelledCancelled order total, positive

refund and order_cancelled are positive on purpose. The event name carries the direction; a negative value subtracts from a platform's conversion value in some integrations and adds to it in others.

When value is omitted on an event that should have one, Mythic derives it as sum(price × quantity) — so a single-item view_item or add_to_cart carries revenue without you computing it.

Example

mythic.capture('purchase', {
  transaction_id: '4581201',
  checkout_id: 'chk_9f2a1c',
  affiliation: 'shop.example.com',
  currency: 'USD',
  value: 107.5,        // total charged
  subtotal: 95,
  shipping: 5,
  tax: 7.5,
  discount: 10,
  coupon: 'SUMMER',
  item_count: 3,
  items: [
    {
      item_id: '7734115',
      item_variant_id: '42901337',
      sku: 'SNOW-01',
      item_name: 'The Collection Snowboard',
      item_brand: 'Hydrogen',
      item_category: 'Snowboards',
      item_variant: 'Large',
      price: 30,        // UNIT price
      quantity: 3,
      index: 1
    }
  ]
});

What each platform receives

You send the spec once. Mythic projects it per platform, in both the browser and the server half.

PlatformWhere the projection runsWhat it gets
Meta PixelBrowser, inside the managed tagvalue, currency, contents[{id, quantity, item_price}], content_ids, content_type, num_items, content_category, order_id, search_string
Meta CAPIServer destinationThe same fields, built by the same projection, with the same event_id — so Meta collapses the pair into one conversion
Google AdsServer destination (Data Manager)conversionValue, currency, transaction_id, and cartData.items[{itemId, merchantProductId, quantity, unitPrice}]
Google AdsBrowser, when a conversion label is setvalue, currency, transaction_id, items[]
GA4Server destination (Data Manager, tagless)Event params, conversionValue, currency, transactionId, and the same cartData as Google Ads
OpenAI AdsServer destinationThe event translated to their taxonomy, with the amount in the currency's minor unit

Meta's contents[].id prefers your sku, then item_variant_id, then item_id — a Shopify catalogue feed keys on the SKU. An item with none of the three is dropped rather than sent with an empty id, because an unmatched content counts against the pixel's match rate.

The Meta projection runs twice, once per half, from one shared implementation. That matters because Meta keeps only one of the pair: if the browser sent line items and the server did not, which payload survived would depend on which arrived first.

Connecting a platform

Each managed platform is one call, and the commerce mapping is filled in for you:

  • POST /client/v1/destinations/integrations/meta — pixel id
  • POST /client/v1/destinations/integrations/google_ads — customer id + conversion actions
  • POST /client/v1/destinations/integrations/google_analytics — measurement id + property id
  • POST /client/v1/destinations/integrations/openai_ads — pixel id

GA4 and OpenAI Ads are server-only: there is no browser tag to install, which is the point — every event arrives with resolved identity and full line items even when the visitor blocks scripts.

Where you pass your own event_mapping, it is merged over the commerce default, so renaming one event never stops the others being delivered. cart_data is the opposite: passing it replaces the default outright, because a partial cart config is a deliberate statement about where your items live.

OpenAI Ads has a closed event taxonomy. Events with no counterpart there — view_item_list, view_cart, remove_from_cart, add_shipping_info, add_payment_info, checkout_step_completed, refund, order_cancelled — are deliberately left unmapped, and an unmapped event is skipped rather than rejected. Everything else still reaches Meta, Google Ads and GA4.

Checking that you actually send it

The spec ships as an Event Contracts pack. Applying it makes drift visible instead of silent:

curl -X POST "https://mythic-analytics.gulp.workers.dev/client/v1/contracts/presets/commerce?location_id=YOUR_LOCATION_ID" \
  -H "Authorization: Bearer ak_YOUR_AGENCY_KEY"

A producer that stops sending value, or starts sending items as an object, shows up in the violation report the next day rather than reaching your ad platforms as a conversion worth zero. Add ?dry_run=true first to see exactly what would be stored.

Contracts stay read-only: nothing is rejected, quarantined or dropped at ingestion because of one.