Channel-Specific CAC (Customer Acquisition Cost)

Channel-specific CAC is the average cost of acquiring a customer through one acquisition channel — Google Search, Meta, LinkedIn, SEO, events — taken on its own. Where blended CAC averages everything together, this isolates each one.

\[ \text{CAC}_{\text{channel}} = \frac{\text{total spend on the channel}}{\text{customers acquired from the channel}} \]

Spend on a channel means the ad spend, agency fees, tools and creative costs specific to it; customers acquired means those attributable to it. Shopify’s marketing reports define CAC in the same shape for a campaign: total advertising and sales spend divided by the first-time customers attributed to that campaign. A month might give Google Ads \$30,000 for 200 customers (\$150), Meta \$20,000 for 250 (\$80), and SEO \$10,000 for 500 (\$20) — three very different efficiencies, all invisible in a blended figure.

That is the case for the metric, and it is a good one: it shows which channels are cost-effective, it gives a historical reference for where the next budget might go (with the limits described at the end), and combined with lifetime value it gives a per-channel return. It also rests entirely on the phrase “attributable to it”.

The credit rule decides the answer

The journeys, spend and customer counts in this section are hypothetical and illustrative, constructed to show how a credit rule changes the result; they are not data from any company.

Same customers, same invoices, three credit rules

Start with what is fixed: 1,700 customers, grouped by the ordered list of channels each one touched before buying, and what each channel cost. Nothing below changes the customers or the money — only the credit rule.

journeys = {                      # touch order -> number of customers
    ("search", "email", "organic"): 300,
    ("social", "search"): 250,
    ("organic",): 400,
    ("social",): 150,
    ("search",): 200,
    ("social", "email", "search"): 180,
    ("organic", "email"): 120,
    ("email", "organic"): 100,
}
spend = {"search": 90_000, "social": 60_000, "email": 12_000, "organic": 30_000}
total_customers = sum(journeys.values())

print(f"{total_customers:,} customers, {sum(spend.values()):,} dollars,"
      f" blended CAC {sum(spend.values()) / total_customers:,.2f}")
1,700 customers, 192,000 dollars, blended CAC 112.94

Three rules then turn those journeys into customers per channel. Last touch gives a customer wholly to the final channel in the journey, first touch to the first, and linear splits one customer equally across every touch. For each rule, channel CAC is the channel’s spend divided by the credit it received.

def credit(rule):
    out = {k: 0.0 for k in spend}
    for path, n in journeys.items():
        if rule == "last":
            out[path[-1]] += n
        elif rule == "first":
            out[path[0]] += n
        else:                              # linear: split equally over the touches
            for k in path:
                out[k] += n / len(path)
    return out

rules = {r: credit(r) for r in ("last", "first", "linear")}
for r, credited in rules.items():
    print(f"{r} touch")
    for k in spend:
        print(f"  {k:>8}: {spend[k]:>6,} / {credited[k]:>5,.1f} = {spend[k] / credited[k]:>6.2f}")
last touch
    search: 90,000 / 630.0 = 142.86
    social: 60,000 / 150.0 = 400.00
     email: 12,000 / 120.0 = 100.00
   organic: 30,000 / 800.0 =  37.50
first touch
    search: 90,000 / 500.0 = 180.00
    social: 60,000 / 580.0 = 103.45
     email: 12,000 / 100.0 = 120.00
   organic: 30,000 / 520.0 =  57.69
linear touch
    search: 90,000 / 485.0 = 185.57
    social: 60,000 / 335.0 = 179.10
     email: 12,000 / 270.0 =  44.44
   organic: 30,000 / 610.0 =  49.18

The customers, their journeys and the invoices are identical across the three rules; only the rule for assigning credit changes. Social costs \$400.00 per customer under last touch and \$103.45 under first touch. Email costs \$100.00, \$120.00 or \$44.44. These are not estimates with error bars around a true value — they are different questions, and each rule answers its own correctly.

The ranking, and the decision, change with the rule

for r in rules:
    order = sorted(spend, key=lambda x: spend[x] / rules[r][x])
    print(f"{r:>6}: " + " < ".join(f"{x} {spend[x] / rules[r][x]:.2f}" for x in order))
  last: organic 37.50 < email 100.00 < search 142.86 < social 400.00
 first: organic 57.69 < social 103.45 < email 120.00 < search 180.00
linear: email 44.44 < organic 49.18 < social 179.10 < search 185.57

Take the rule “scale the cheapest channel and cut the most expensive.” Under last touch the most expensive channel is social at \$400.00, so the rule cuts social. Under first touch the most expensive channel is search at \$180.00, so the rule cuts search instead, and social, at \$103.45, is the second cheapest. The channel ranking changes, while organic is the cheapest under both single-touch rules. Single-touch rules sit at the two ends of a journey: last touch flatters channels near the conversion, first touch flatters channels that start journeys. Linear splitting divides each customer’s credit equally across that customer’s touches, so it sits between them in where credit goes within a journey, but not necessarily in the resulting channel CAC or ranking. Here email costs \$44.44 under linear splitting, below both its last-touch (\$100.00) and first-touch (\$120.00) figures. Linear splitting is not more correct, only a different rule. Choosing a model before looking at the numbers is the only way to keep the choice from being made by the answer it produces.

The arithmetic has to close

Each of the three rules assigns one full customer of credit in total, so each one credits exactly the real customer count. Platform-reported conversions are different: each platform claims every journey it saw, so a customer who touched three channels is claimed three times. The code below totals the credit under each rule, then counts what each platform would claim.

for r in rules:
    print(f"{r:>6} touch credits {sum(rules[r].values()):,.0f} customers"
          f" against {total_customers:,} actual")

claimed = {k: sum(n for path, n in journeys.items() if k in path) for k in spend}
multi = sum(n for path, n in journeys.items() if len(path) > 1)
print(f"{multi:,} of {total_customers:,} customers touched more than one channel")
for k in spend:
    print(f"  {k:>8} claims {claimed[k]:>3,} customers -> reports CAC {spend[k] / claimed[k]:>6.2f}")
print(f"total claimed {sum(claimed.values()):,} against {total_customers:,} real customers")
  last touch credits 1,700 customers against 1,700 actual
 first touch credits 1,700 customers against 1,700 actual
linear touch credits 1,700 customers against 1,700 actual
950 of 1,700 customers touched more than one channel
    search claims 930 customers -> reports CAC  96.77
    social claims 580 customers -> reports CAC 103.45
     email claims 700 customers -> reports CAC  17.14
   organic claims 920 customers -> reports CAC  32.61
total claimed 3,130 against 1,700 real customers

Any rule that divides each customer’s credit exactly once totals to the real customer count, and its channel CACs reconcile with blended CAC if the costs also partition the same total. Platform-reported conversions do not work that way — each platform claims every journey it touched, so the claims overlap. Summing them here gives 3,130 customers against 1,700 real ones, 84% too many, and every resulting channel CAC is below the blended figure. All channel CACs being lower can signal double-counted customers, but can also result from overhead included only in blended CAC. Compare both numerator scope and attribution before concluding.

The other two difficulties

Lag is the second one. Investment in SEO and content produces customers months later, so a channel measured on same-period spend over same-period customers looks inflated while it is being built and cheap after it is cut. The organic CAC entry puts numbers on that; it applies to any channel with a delay between spending and converting, which includes most brand and event work.

Hidden cost is the third. Marketing salaries, brand building and shared tooling are not naturally attributable to one channel, so they either sit outside every channel CAC — making all of them look better than the blended figure for a second reason — or get allocated by an assumption that the fully loaded CAC entry examines.

Average CAC also does not answer the question of the next dollar. A channel CAC is an average over spend that has already happened, assigned by an attribution rule, so it describes past attributed efficiency. Extra spend buys the marginal (incremental) customers that the added money brings in, and that cost can differ from the average: a channel that averaged \$150 per customer may reach audiences it already covered with the next \$10,000 and add fewer than the 67 customers the average implies, or it may add more. This is a hypothetical illustration, not a finding. Judging additional budget therefore needs an estimate of incremental customers (a response curve fitted to spend variation or measured in experiments) together with customer value, margin, and any capacity or budget constraints, not the average CAC alone.

None of this argues against the metric. Channel-specific CAC is the attributed-efficiency measure and blended CAC is the health check, and a marketing manager deciding where the next dollar goes should start from the former while treating it as one input rather than a forecast of added customers. It argues for three things traveling with the number: the attribution model, the time window, and whether the channel figures roll up to the blended one. Where the stakes are high enough, a holdout test answers the incrementality question that no attribution rule can.

References

  • Shopify Help Center, Marketing performance (campaign attribution report metrics, including customer acquisition cost). help.shopify.com

Discover more from Insightful Data Lab

Subscribe to get the latest posts sent to your email.

Similar Posts

Questions, corrections, or additional insights?

This site uses Akismet to reduce spam. Learn how your comment data is processed.