> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Marketing mix model — split a budget across channels from spend and revenue history

> Operator guide to the Insights → Marketing Mix Model surface: regress channel spend against revenue over time with adstock decay and diminishing-returns response curves, then read the recommended budget split — plus API access and when attribution is the better tool.

# Marketing mix model

The **Insights → Marketing Mix Model** page answers one question: **given your channel spend and revenue history, how should you split a budget across channels?** Type the revenue event your tracking plan records, set the property names you carry channel, spend, and value on, pick a lookback window and a carryover rate, and run — the page regresses your aggregate channel spend against your aggregate revenue over time and returns a recommended budget split with per-channel ROI and a diminishing-returns response curve.

This is a top-down report over aggregates, complementing the bottom-up surfaces: [Multi-touch attribution](/guides/multi-touch-attribution) shares credit for conversions across touchpoints, and holdout/lift tests whether one campaign caused lift. The mix model answers the budget question, not the credit question.

## What the surface computes

The model buckets your spend and revenue into aligned daily periods over the lookback window, applies **adstock decay** to each channel's spend (a spend day keeps influencing revenue for a while, at a declining rate), then fits a regression of daily revenue against all channels' decayed spend at once. On each channel it applies a **diminishing-returns response curve** so the fit knows that doubling spend returns less than the first spend unit did.

The page then reports per-channel **contribution** (the revenue the fit assigns to that channel), **ROI** (contribution ÷ spend), a per-channel **response-curve sparkline** (expected revenue at spend multipliers from zero up), and a **recommended spend per day per channel** — the split that maximizes expected revenue given diminishing returns. Summary tiles above the table give the **fit quality (R²)**, total revenue, revenue attributed to channels, and the **baseline revenue per day** (what recurs with zero spend).

## Inputs

Open **Insights → Marketing Mix Model** and set:

* **Revenue event** — the event name your SDKs emit when money is recognized (commonly `Order Completed`). Required. The event must already exist in your CDP; a typo fits nothing.
* **Date range** — 14, 30, 60, 90, or 180 days. Longer windows give the regression more daily periods to separate channels; shorter windows track recent pricing shifts.
* **Carryover (adstock decay)** — 0 to 0.95, default 0.3. How much of one day's spend persists into the next day. Near zero models immediate, transactional media; 0.5–0.7 suits awareness channels whose effects linger.
* **Channel property** — the event property that names the channel (`channel` by default).
* **Spend property** — the property carrying spend for that channel (`cost` by default).
* **Revenue property** — the property carrying the currency value on the revenue event (`value` by default).

Run the model when the revenue event field is non-empty. Re-run whenever an input changes — every run recomputes from scratch with no caching.

### Choose the event and property set per channel

The property names are a mapping between your tracking plan and the model — the event you name must carry a numeric value, and every other event that carries channel spend must record it under the same property pair you enter. Examples across channel types:

* **E-commerce purchase:** revenue event `Order Completed`, revenue property `total`, channel property `utm_source` or a campaign-scoped `channel` field.
* **Messaging-cent (SMS/WhatsApp spend):** campaigns and notifications already emit per-channel spend — point the spend property name at the field your accounting events use (`cost` is the default for a reason).
* **LLM / AI-agent spend:** if you track agent usage as a cost stream, model it with the same shape — one event carrying the agent's spend tagged by a channel property so the model can weigh it against revenue.

The deciding test: for **each row of spend**, the model needs one aligned day-bucket of channel, spend, and revenue. Events that record spend on one naming convention and revenue on another fit nothing — align the property names in the form, or fix the tracking plan on **CDP → Tracking plan** and re-run.

## Outputs

After a successful fit the page renders, top to bottom:

1. **Summary tiles** — fit quality (R²), total revenue in the window, revenue attributed to channels, and baseline revenue per day. Treat R² as a sanity gate: a fit near zero means the channels explain almost none of the revenue variance and the recommended split is not worth acting on.
2. **Channel contribution table** — one row per channel with:
   * **Contribution** — attributed revenue, with a bar scaled to the highest contributor.
   * **Share** — the channel's fraction of total attributed revenue.
   * **ROI** — contribution ÷ spend; `—` when the channel had no spend.
   * **Response curve** — a sparkline of expected revenue as spend scales, so a flattening curve shows saturation without reading the table.
   * **Current spend/day** and **Recommended/day** — the second with a `+`/`−` delta against current when the model suggests moving budget.
3. **Channel-adjustment deltas** — green `+Δ` (spend up) and red `−Δ` (spend down) markers beside recommended spend, so budget moves read at a glance.
4. **Fit metadata footer** — the number of daily periods used, the resolved adstock decay, and the exact since/until window rendered in your workspace timezone.

When more channels record spend in the window than the model fits at once (the cap is 12), the page shows a warning banner that only the top channels by spend are included.

## When the data is not enough — the 422 case

Too little aligned spend-and-revenue history is the expected failure, not a crash: the API returns HTTP 422 and the page renders an inline **"Not enough history yet"** state instead of an error toast. The model needs at least two weeks of aligned daily spend and revenue. Widen the date range, confirm the revenue event name, and confirm the channel, spend, and value property names match what your tracking plan actually emits. Any other failure (missing scope, a malformed body) surfaces as a normal error card with the API's message.

## When NOT to use the mix model

* **Noisy seasonality drowned out in your window** — if a holiday spike or outage dominates the revenue series, the regression attributes it to whichever channels spent that week. Exclude the spike or choose a cleaner window before trusting the split.
* **Insufficient spend diversity** — if every channel's daily spend rises and falls together (all-or-nothing campaign flights), the model cannot separate their contributions; R² will look fine while the per-channel numbers are meaningless. Prefer periods where channels varied independently.
* **Attribution already answers the question** — the mix model allocates *budget*, not *credit*. For "which touchpoints share this conversion," use [Multi-touch attribution](/guides/multi-touch-attribution); for "did this campaign cause lift," use a holdout test. Solving the credit question with a budget model — or vice versa — produces confident wrong answers.

## API access

Everything on the page runs off one compute-on-demand POST against your API key — there is no GET for a saved report. The endpoint requires an `owner`, `admin`, or `developer` key with the `contacts:read` scope, same gate as every CDP analytics surface.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/cdp/analytics/marketing-mix-model \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "revenue_event": "Order Completed",
    "channel_property": "channel",
    "cost_property": "cost",
    "value_property": "value",
    "adstock_decay": 0.3,
    "since": "2026-06-19T00:00:00Z",
    "until": "2026-09-16T23:59:59Z"
  }'
```

Request fields: `revenue_event` (required, 1–200 characters), `channel_property` / `cost_property` / `value_property` (defaults `channel` / `cost` / `value`), `adstock_decay` (0–0.95, default 0.3), optional `channels` (explicit channel allowlist, at most 12 — omit to auto-select the top channels by spend), optional `total_budget` (cap for the recommended split), and optional `since` / `until` ISO datetimes.

A successful response returns `status: "fitted"` with `periods`, `adstock_decay`, `r2`, `baseline_revenue_per_period`, `total_revenue`, `total_attributed_revenue`, a `channels` array (`channel`, `coefficient`, `contribution`, `contribution_share`, `roi`, `avg_spend`, `response_curve` points, `recommended_spend`, `recommended_spend_delta`), and the echoed inputs including `channels_considered`. Non-fitted responses come back as 422. Pin `since` / `until` in a scheduled job to make runs comparable — new events landing changes yesterday's window.

## Troubleshooting

| Symptom                                 | Why / fix                                                                                                                                           |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| 422 — not enough history                | Under two weeks of aligned daily spend and revenue. Widen the window or verify event/property names against the tracking plan.                      |
| 403                                     | The key needs an owner/admin/developer role with `contacts:read`.                                                                                   |
| Zero channels fitted                    | No spend was recorded under the channel property in the window. Check `channel_property` against your accounting events.                            |
| R² near zero                            | Channels explain almost none of the revenue variance — treat the recommended split as noise.                                                        |
| Warning banner about truncated channels | More than the fitting cap of 12 channels recorded spend; only the top by spend are modeled. Pass an explicit `channels` list via the API to choose. |
| Numbers differ from yesterday's run     | The report recomputes at request time with no caching. Pin `since` / `until` for comparable runs.                                                   |

## See also

* [Attribution on insights](/guides/insights-attribution) — the bottom-up credit-sharing surface this budget model complements
* [Build conversion funnels and retention over CDP events](/guides/insights-funnels-cdp-conversion) — sibling compute-on-demand CDP analytics surface
* [Insights dashboards reading](/guides/insights-dashboards) — where the mix model sits among Insights surfaces
* [CDP tracking plan](/guides/cdp-tracking-plan) — keep spend and revenue property names aligned before fitting
