Saleor Pulse Commerce Context
Commerce Context is an open note on the Saleor order: sectioned metadata under commerce.context.*. Storefronts, POS, imports, and agents write facts. Pulse reads the order and shows Origins on Financial β surface, campaign, or source, ranked by net after refunds.
The note is the join. Saleor stays the commerce record. Pulse is a reader. Write the note before checkoutComplete.
What Pulse showsβ
Origins sits on Financial, on the same period, currency, and channel as the rest of that view. The subtitle is coverage: tagged revenue orders over all revenue orders. Unconfirmed orders count. Cancelled ones do not.
Pulse ranks tagged orders only. Marketing on the note is first-touch. Net after refunds is a books figure, not a lift study. Click IDs for Meta or Google stay on your ads path.
A document is useful as soon as origin.surface parses. Campaign and source stay empty until you write marketing. Untagged surfaces do not rank as worse β they are absent until they write the note.
Design rulesβ
- Metadata on the checkout β Saleor copies it onto the order at complete. See Metadata.
- Sectioned keys β each section is its own metadata key (
origin,marketing,session), not one combined JSON blob. Saleor replaces a whole key. Do not read-merge-write someone else's section. - The reader composes β Pulse (or any consumer) joins the keys. Writers attach facts.
- No people in the note β the order already links the customer. No emails, names, or tokens.
- Never block checkout β if the metadata write fails, complete the order anyway.
Keysβ
| Key | Owner | Contents |
|---|---|---|
commerce.context.origin | Checkout surface (storefront / POS / import / draft) | surface (required), system, capturedAt, consent |
commerce.context.marketing | Storefront / marketing tag layer | First-touch UTM-like fields + landingPath |
commerce.context.session | Storefront | sessionId when storage is allowed |
commerce.context.actors | Affiliate / sales / agent / support apps | Write only if you are that system |
commerce.context.experiment | Experiment / flag tool | id, variant |
commerce.context.ext.<vendor> | That vendor | Your own JSON bag |
There is no shared commerce.context.properties key in v1. It invites silent overwrites. Pulse folds each ext.<vendor> object into composed properties.<vendor>.
Channel stays on the Saleor order, not in this note.
originβ
{
"surface": "storefront",
"system": "paper",
"capturedAt": "2026-09-13T10:00:00.000Z",
"consent": "unknown"
}
| Field | Required | Description |
|---|---|---|
surface | yes | How the order was created |
system | recommended | Writer identity β paper, your app id, a register build |
capturedAt | no | ISO-8601 timestamp when this origin object was attached |
consent | recommended for storefronts | Shopper consent for storage-derived sections |
device | no | mobile | tablet | desktop | other. From the user agent; no storage, so no consent needed |
origin is never gated on cookie consent. It describes the order, not the person.
surfaceβ
Pulse recognizes storefront, pos, draft, import, marketplace, api, agent, and support. That list will grow. Pick the closest match. Put the writer in system. Do not invent a surface for a device or a brand.
A native shopping app that creates checkouts is still storefront. A till is pos. A Dashboard staff order is draft.
consentβ
granted | denied | not_required | unknown
Whether storage-derived sections (marketing, session) are allowed. Origin itself is never consent-gated. not_required covers an implied storefront regime or a writer with no shopper cookie (till, import, agent). unknown is a missing decision β Paper's default required mode with no banner β not a till.
| Value | Meaning | Sibling sections |
|---|---|---|
granted | Consent given for analytics storage | marketing / session present when data existed |
denied | Shopper said no | marketing / session absent |
not_required | No shopper opt-in needed β implied storefront regime, or no shopper cookie (till, import, agent) | marketing / session present when data existed |
unknown | Consent primitive exists but no decision yet (Paper required with no banner), or not recorded | any; Paper skips marketing |
Write denied when the shopper said no β do not omit the field. Without it, a declined visit looks like direct traffic.
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; do not merge fields across writes.
Strip tracking query params from landingPath before you write. Pulse redacts common secrets again on ingest.
sessionβ
A per-visit sessionId when storage is allowed. Do not write anonymousId in v1.
Two writesβ
Paper is the reference storefront. Copy the keys, not the React tree. The builders live in saleor/storefront under src/lib/commerce-context/.
Saleor 3.21+ accepts metadata on checkoutCreate. Pulse and Paper target 3.23+.
On create β originβ
Put origin (and your ext.<vendor> bag) in checkoutCreate. Zero extra round trips. Present even if the shopper abandons before complete.
mutation CheckoutCreate(
$channel: String!
$lines: [CheckoutLineInput!]!
$metadata: [MetadataInput!]
) {
checkoutCreate(
input: { channel: $channel, lines: $lines, metadata: $metadata }
) {
checkout {
id
}
errors {
field
code
}
}
}
[
{
"key": "commerce.context.origin",
"value": "{\"surface\":\"storefront\",\"system\":\"paper\",\"capturedAt\":\"2026-09-13T10:00:00.000Z\",\"consent\":\"unknown\"}"
},
{
"key": "commerce.context.ext.paper",
"value": "{\"locale\":\"en-US\",\"paperVersion\":\"223382c0\"}"
}
]
Before complete β marketing and sessionβ
Write these with updateMetadata before checkoutComplete, and only when origin.consent is granted or not_required. Saleor copies checkout metadata onto the order at complete. Pulse reads the order.
mutation EnrichCheckoutContext($id: ID!, $input: [MetadataInput!]!) {
updateMetadata(id: $id, input: $input) {
item {
... on Checkout {
id
}
}
errors {
field
code
message
}
}
}
[
{
"key": "commerce.context.marketing",
"value": "{\"source\":\"google\",\"medium\":\"cpc\",\"campaign\":\"summer_sale\",\"landingPath\":\"/collections/summer\"}"
},
{
"key": "commerce.context.session",
"value": "{\"sessionId\":\"sess_01J8β¦\"}"
}
]
If consent changed between create and complete, re-send the whole origin key. Keep the original capturedAt when you have it.
Paper's default consent mode is required, and core Paper ships no banner. Create-time consent is then unknown, this second write is skipped, and Pulse still treats the order as valid from origin alone. That is a complete origin note. You get surface on Origins. Campaign and source stay empty until you allow first-touch cookies.
If the metadata write fails, complete anyway. Pulse will show surface without a campaign β the same empty campaign as consent unknown. A thin note beats a failed payment.
Other checkoutsβ
Change surface and system. Keep the key names.
{
"commerce.context.origin": {
"surface": "pos",
"system": "register-ios",
"capturedAt": "2026-09-13T10:00:00.000Z",
"consent": "not_required"
}
}
An agent checkout is the same idea β no shopper cookie, so not_required β with surface: "agent" and actors if you own that identity. Do not reuse a storefront origin builder or copy the shopper's first-touch cookie.
{
"commerce.context.origin": {
"surface": "agent",
"system": "paper",
"capturedAt": "2026-09-13T10:00:00.000Z",
"consent": "not_required"
},
"commerce.context.actors": {
"agent": { "type": "mcp", "id": "β¦" }
}
}
| 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 own payload; consent: not_required; actors if you own that identity |
What not to writeβ
- Emails, names, tokens, click IDs you are not prepared to treat as durable commerce data
- Tracking query params on
landingPath anonymousId(reserved in v1)- Someone else's section
- A new
surfacefor βmobileβ or a brand name
Next stepsβ
- Pulse overview β where Origins sits on Financial
- Data and privacy β what Pulse keeps
- Metadata API
- Paper β reference write in
src/lib/commerce-context/