Documentation menu

Outcomes

The order join: report orders through POST /v1/outcomes or the Shopify orders webhook, and the funnel attributes them to agent traffic with labeled evidence — explicit, lookback-inferred, or none. Nothing is guessed.

Closing the funnel

Events record who visited and what they were served. Outcomes record what it earned you: one row per reported order, joined onto the visit stream at read time. Together they power the Console funnel’s fourth stage — agent visits → machine-readable served → protocol requests → orders reported — and the per-platform and per-URL order columns.

There are two ways in, and they land in the same table with the same idempotency rule: the outcomes API (any stack) and the Shopify orders webhook (zero code on Shopify).

POST /v1/outcomes

Authenticated with your store-scoped API key — the same key the events sink uses, sent as a Bearer token. The store is always resolved from the key, so a payload can never write another store’s orders.

POST /v1/outcomes
POST {api}/v1/outcomes
Authorization: Bearer <apiKey>
Content-Type: application/json

{ "orders": OutcomesOrderV0[] }   // max 100 per request
FieldTypeNotes
order_idstring (required)Your order identifier — non-empty. Part of the idempotency key.
tsstring (required)Order timestamp, ISO 8601.
order_value_centsnumber (required)Non-negative integer minor units (cents). Never floats, never converted.
currencystring (optional)Defaults to USD when absent. Stored as reported — no conversion anywhere.
product_urlsstring[] (optional)Canonical product URLs on the order. These power the 7-day lookback inference and the Products page’s “orders touching URL” column.
agent_attributedboolean (optional)Defaults to false. Send true only when you captured real evidence — and put that evidence in attribution. The API stores exactly what you say; it never upgrades a claim.
attributionobject (optional)Your evidence, free-shape (utm, referrer, session note). attribution.platform (e.g. "chatgpt") lets the funnel’s platform table count the order as explicit evidence for that platform; attribution.channel: "protocol" marks a protocol-originated order for the observational take-rate metering view.
event_idstring (optional)UUID of the originating gateway event, when you carried it through your session — the strongest join evidence.
A fully evidenced order
{
  "orders": [
    {
      "order_id": "1001",
      "ts": "2026-08-05T12:00:00Z",
      "order_value_cents": 4999,
      "currency": "USD",
      "product_urls": ["https://store.example.com/products/alpine-trail-pack-28l"],
      "agent_attributed": true,
      "attribution": { "platform": "chatgpt", "utm_source": "chatgpt.com" },
      "event_id": "018f7c9a-1b2e-7c3d-9e4f-5a6b7c8d9e0f"
    }
  ]
}
  • Success: 202 with { "accepted": <n> } (n = orders submitted). An empty orders array is a valid no-op — 202 {"accepted": 0}.
  • Idempotent: replays are absorbed by the database — the insert is ON CONFLICT (store_id, source, order_id) DO NOTHING, so resending an order is always safe.
  • 401: unknown, revoked, or missing API key.
  • 400: malformed body — orders not an array, more than 100 orders per request (split client-side), or a structural failure on any order (the error names the field).
  • 503: the outcomes database is not configured on that deployment.

The Shopify orders webhook

On Shopify, orders flow in with no storefront code at all — a standard orders/create webhook:

  1. In Shopify admin (or your app configuration), create an orders/create webhook pointing at POST /webhooks/shopify/orders on the Rebilder API.
  2. Set the webhook’s signing secret as SHOPIFY_WEBHOOK_SECRET on the API deployment. This is distinct from SHOPIFY_APP_SECRET (the app-proxy signature secret the gateway’s Shopify adapter uses) — the two verify different surfaces and are never interchangeable.
  3. That’s it. Deliveries are verified before anything is parsed: X-Shopify-Hmac-Sha256 must equal base64(HMAC-SHA256(raw body)) — computed over the raw bytes, compared constant-time. A bad or missing signature is 401.

Shop-domain resolution: the X-Shopify-Shop-Domain header is matched (lowercased) against your store’s registered domain. An unmatched or missing shop domain returns 202 {"accepted": 0, "unmatched": true} and is logged — never an error status, so Shopify does not retry a delivery that can never be attributed. Product URLs are derived only from line items that carry a handle (https://{shop}/products/{handle} — Shopify’s canonical product path); product identity is never guessed from titles or ids.

The attribution model

Attribution is evidence-based and computed at read time. Every order is classified into exactly one of three classes:

ClassWhat it meansWhere it comes from
explicitThe reporter sent evidence with the order.agent_attributed: true plus the evidence in attribution (utm, referrer, event_id) — API-reported orders only.
inferredThe 7-day product-URL lookback fired: agent traffic hit a product URL on this order within the 7 days before it.A read-time database view — the inference is labeled as inference and never stored back onto the order.
noneNo evidence either way.The honest default for everything else.

How the Console consumes outcomes

  • Funnel — stage 4 (“Orders reported”) shows total orders and order value as reported, split into explicit / lookback-inferred / unattributed; the platform table adds “orders with evidence” per platform; the daily bars show orders per UTC day.
  • Products — “Orders touching URL” counts orders whose reported product_urls include that exact URL: presence on the order, not attribution credit.
  • Insights — flags “No orders reported” when agent traffic exists but no outcome source is wired, with the fix pointing back to this page.

Everything above is an observed count over what was reported — no currency conversion, no extrapolation, no modeled revenue. If a number is inference, it is labeled inference.