← Blog

Announcement,

Pulse: Introducing Commerce Context protocol

Mirek Mencel

A small metadata note on the Saleor order. Pulse ranks the origins that still have net after refunds — and any checkout can write the note, including a mobile app.

Session tools count visits. Saleor counts money. Neither can say, on the order, where that sale came from.

That is the gap Pulse and Commerce Context are built to close. Write the note before complete. The join lives on the commerce record.

Every order can carry a small note about where it came from.

Pulse ranks those origins by the money that remains after refunds.

Saleor shops are multi-surface. POS, draft, import, agent, and storefront are not campaign parameters. One note language puts them on the same table as campaign and source.

Origins in Pulse is that table: tagged revenue orders, first landing stored on the note, ranked by net after refunds in the Financial window. A holdout still answers incrementality. This answers the books.

The join on the order

Evidence on the order. Origins in Pulse

Saleor stays the commerce record. Writers attach facts. Pulse is a reader.

Commerce Context is an open format for that note. Storefronts and apps write sectioned metadata under commerce.context.*. Pulse composes those keys when it ingests the order, then shows Origins on Financial: surface, campaign, or source.

The write shape and compose rules live in the Commerce Context Spec. Pulse ships the reader. Copy the keys. Let the reader compose.

Click IDs for Meta or Google stay on your ads path. Use Pulse for revenue on the books. Use the analytics tool for funnel shape.

What you write

Sectioned keys, one owner each

Write each section as its own metadata key — origin, marketing, session — not one combined JSON blob. Saleor replaces a whole key on write. Do not read-merge-write someone else's section.

A document is useful to Pulse as soon as origin.surface parses. Marketing alone is only a partial note.

JSON
1{
2 "commerce.context.origin": {
3 "surface": "storefront",
4 "system": "paper",
5 "capturedAt": "2026-09-13T10:00:00.000Z",
6 "consent": "unknown"
7 }
8}

surface is how the order was created. Pulse v1 recognizes storefront, pos, draft, import, marketplace, api, agent, and support — that list will grow. Pick the closest match. Put the writer in system (paper, your app id, a register build). Do not invent a surface for a device or a brand. Channel stays on the Saleor order.

01

origin

Who created the checkout. Required. Never gated on cookie consent — it describes the order, not the person.

02

marketing

First-touch UTM fields plus a redacted landingPath. Write once, fill-missing. Pulse ranks campaign and source on this first landing. Last-touch is a different policy — own the whole key if you need it.

03

session

A per-visit sessionId when storage is allowed. Do not write anonymousId in v1.

04

ext.<vendor>

Your own JSON bag. Paper writes ext.paper (locale, baseline). Pulse folds it into properties.

05

actors / experiment

Affiliate, sales, agent, support, or a flag tool. Write these only if you are that system.

06

consent

Lives on origin: granted, denied, not_required, unknown. Without it, a declined visit looks like direct traffic.

Paper as the reference

Two writes. Same GraphQL your app already has.

Paper is the reference storefront. Copy the keys, not the React tree.

Saleor 3.21+ accepts metadata on checkoutCreate (Pulse and Paper target 3.23+). Paper puts origin and ext.paper in that mutation — zero extra round trips, present even if the shopper abandons before complete.

GRAPHQL
1mutation CheckoutCreate($channel: String!, $lines: [CheckoutLineInput!]!, $metadata: [MetadataInput!]) {
2 checkoutCreate(input: { channel: $channel, lines: $lines, metadata: $metadata }) {
3 checkout {
4 id
5 }
6 errors {
7 field
8 code
9 }
10 }
11}
JSON
1[
2 {
3 "key": "commerce.context.origin",
4 "value": "{\"surface\":\"storefront\",\"system\":\"paper\",\"capturedAt\":\"2026-09-13T10:00:00.000Z\",\"consent\":\"unknown\"}"
5 },
6 {
7 "key": "commerce.context.ext.paper",
8 "value": "{\"locale\":\"en-US\",\"paperVersion\":\"223382c0\"}"
9 }
10]

Marketing and session need storage. Paper writes them with updateMetadata before checkoutComplete, and only when origin.consent is granted or not_required. Saleor copies that metadata onto the order at complete. Pulse reads the order.

GRAPHQL
1mutation EnrichCheckoutContext($id: ID!, $input: [MetadataInput!]!) {
2 updateMetadata(id: $id, input: $input) {
3 item {
4 ... on Checkout {
5 id
6 }
7 }
8 errors {
9 field
10 code
11 message
12 }
13 }
14}
JSON
1[
2 {
3 "key": "commerce.context.marketing",
4 "value": "{\"source\":\"google\",\"medium\":\"cpc\",\"campaign\":\"summer_sale\",\"landingPath\":\"/collections/summer\"}"
5 },
6 {
7 "key": "commerce.context.session",
8 "value": "{\"sessionId\":\"sess_01J8…\"}"
9 }
10]

Paper's default consent mode is required, and core Paper ships no banner. Create-time consent is then unknown, tier two is skipped, and Pulse still marks the order valid from origin alone. That is a complete origin note. You get surface on Origins. Campaign and source stay empty until you actually allow first-touch cookies.

The Paper builders live in saleor/storefront under src/lib/commerce-context/. They are pure functions: you can read them from a native app without taking Next.js with you.

Other checkouts

Same keys on iOS, Android, and a register

Change surface and system. Keep the key names.

A native app that creates checkouts is still a storefront. A till is not.

JSON
1{
2 "commerce.context.origin": {
3 "surface": "pos",
4 "system": "register-ios",
5 "capturedAt": "2026-09-13T10:00:00.000Z",
6 "consent": "not_required"
7 }
8}
Writer surface system (example) Marketing
Paper / web storefront storefront paper First-touch, consent-gated
iOS or Android shopping app storefront your app id Same rules if you keep a landing snapshot
POS / register pos register build Usually none
Dashboard draft draft dashboard Usually none
CSV / orderBulkCreate import importer name Usually none
Support or agent placing order support / agent that tool Optional; actors if you own that identity

Set consent to not_required when there is no shopper cookie (a till, an import). Write denied when the shopper said no — and then omit marketing and session. That is how Pulse can keep “direct,” “declined,” and “not captured” apart later.

In Pulse

Origins sits on Financial, on the same period, currency, and channel as the rest of that view. The subtitle is coverage for that window: tagged revenue orders over all revenue orders. Unconfirmed orders count. Cancelled ones do not. Untagged surfaces do not rank as worse — they are absent until they write the note.

Pulse Origins on Financial, grouping surfaces by net revenue after refunds

    Get more useful guides, tech insights, and free learning materials by subscribing to our list.
    All human-written!

    By registering you agree to our Privacy Policy.
    The form is protected by reCAPTCHA - Privacy Policy and Terms of Service.