Analytics

Offline Conversion Reconciliation: Build a Delivery Ledger Before Trusting the Dashboard

Reconcile CRM event cohorts with submission and destination evidence. Implement a tested, duplicate-aware snapshot reducer and distinguish delivery coverage from ad attribution.

Your integration reports successful requests. Your CRM reports a thousand qualified leads. Your ad platform reports something else. Before deciding that attribution is broken, establish a smaller fact: which eligible business events have a supported delivery outcome at a precisely defined destination boundary?

Editorial note: AI-assisted research, writing and implementation by Growthcraft Editorial. This is an original operational design with synthetic data, not a description of an Akshay client engagement. No personal review by Akshay, platform endorsement or production API execution is claimed.

Four practical takeaways

  • A successful HTTP request, destination ingestion, ad matching, attribution and incremental impact are different checkpoints. A delivery ledger covers only the checkpoints its evidence supports.
  • Count unique eligible events per destination, not upload attempts. Fix the event-time cohort and snapshot cutoff before comparing states.
  • Show accepted coverage of the eligible population beside acceptance among terminal outcomes. Otherwise, a healthy-looking rate can hide unsent and unresolved records.
  • Make unknown evidence an explicit state. A missing diagnostic is not proof of rejection, and a warning count is not a count of failed conversions.

Why revisit the upload boundary now?

Google's Data Manager API v1.8 release, dated 30 July 2026, introduced field-level ingestion warnings. Its release-notes page was updated on 15 September. That is a timely engineering signal to review how integrations retain diagnostics; it is not evidence of keyword demand or a prediction of advertiser performance. Google release notes.

Google documents a follow-up diagnostics workflow using the request ID after successful ingestion requests. Its current guide distinguishes processing and terminal destination statuses, and notes that record_count includes successful and failed records. Consequently, treating request receipt or that total as accepted conversions is unsupported. The guide supplies polling timing and backoff guidance; consult the current version for your implementation rather than treating this article as a provider SLA. Data Manager diagnostics.

The practical inference is broader than one API: preserve the evidence at each boundary and name the thing being counted. This article deliberately goes narrower than a general marketing observability programme. It builds the cohort-level reconciliation component that such a programme needs. It does not replace a data contract, attribution model or incrementality study.

Choose a grain that survives retries

Start with one business event and one destination. A qualified lead entering a CRM stage is not the same event as a form submission, and a later sale is not a correction to the lead merely because the customer is the same. Define a business_event_id whose meaning includes the approved occurrence or revision policy. Combine it with destination account and conversion-action identity in your delivery key. The code below assumes those keys have already been created upstream.

Freeze a half-open event-time window, such as 1 September 00:00 UTC through 2 September 00:00 UTC, and separately record an observation cutoff. The cohort answers when the business event occurred; the cutoff answers what was known when you reviewed it. A late upload must not silently move the original event into today's cohort. A revised eligibility rule creates a restated version, not an invisible rewrite of yesterday's denominator.

Eligibility belongs before the transport stage. Exclude test events and events that do not meet the approved business and data-use rules, with reason counts in a separate register. This article does not determine legal eligibility or consent. Have the responsible owner approve those rules and their version. Do not remove difficult-to-upload events merely to improve the delivery percentage.

Use three evidence layers, not one success flag

A minimal design separates a cohort manifest, a submission-attempt log and a destination-state snapshot. The manifest enumerates eligible keys. The attempt log contains immutable attempt IDs, request IDs where available, adapter version and timestamps. The snapshot contains the supported current state for each submitted event at the chosen boundary. Keep actual customer payloads outside the reporting layer; the incident packet needs internal references and aggregate counts.

LayerGrainRequired invariantWhat it cannot prove
Eligible manifestBusiness event × destination × rule versionOne approved membership per keyThat any transmission occurred
Attempt logOne transmission attemptAttempts link back to eligible keysThat receipt means processing success
State snapshotSubmitted key at one cutoff and boundaryOne non-conflicting current stateMatching, attribution or business incrementality

Do not treat this logical schema as a promise that every platform returns row-level outcomes. If diagnostics are only available per request, reconcile their totals at that grain. Do not distribute aggregate failures arbitrarily across event IDs. Record the event-level uncertainty as unclassified. A request containing repeated records or multiple destinations cannot be translated into unique-event acceptance without additional evidence.

For production storage, enforce the manifest key with a database uniqueness constraint. Deduplicate attempt identifiers separately. Keep snapshot versions immutable, with a link to the evidence and adapter used to derive them. Writing a terminal snapshot and acknowledging the worker should follow your durable processing design; an in-memory set is only an illustrative counting tool, not an exactly-once guarantee.

Define five mutually exclusive population buckets

Let E be eligible unique events and S be unique events with a recorded submission. Within S, let A be accepted, R rejected and P explicitly pending. All three describe current states at the same boundary. Unclassified U is the remainder S − A − R − P. Not submitted is E − S. The accounting identity is therefore E = (E − S) + A + R + P + U.

An unknown state is not the same as pending. Pending requires positive evidence that processing is ongoing. Unclassified means the packet lacks enough evidence to select a state. Rejected is a known terminal outcome; it may need remediation, but it is not unresolved in this population definition. Label that distinction on the dashboard so an operational team does not mistake reconciliation completeness for delivery success.

Two rates are particularly useful. Accepted coverage is A/E: how much of the eligible cohort has supported acceptance. Terminal acceptance is A/(A+R): acceptance among events with known terminal states. Neither is a match rate. Report undefined when the denominator is zero. Display rounding belongs at the edge; keep full ratios in exports and computations.

A synthetic incident with a misleading headline rate

Consider a frozen cohort of 1,000 eligible events. There are 900 unique submissions. Evidence supports 720 accepted events, 90 rejected events and 60 pending events. The submission gap is 100; the unclassified gap is 30. Accepted coverage is 72%, while terminal acceptance is 88.89%. The 16.89 percentage-point difference comes from denominator selection, not from an uplift in campaign quality.

There are 190 unresolved events: 100 unsent, 60 pending and 30 unclassified. It would be wrong to say 190 conversions were lost. Some might later be accepted; some might be excluded by a documented correction; some might expose a missing evidence join. It would also be wrong to multiply 190 by an average order value and label that amount lost revenue. Delivery evidence cannot establish a counterfactual business outcome.

The immediate investigation is concrete. The integration owner inspects missing manifest-to-diagnostic links for the 30 unclassified submissions. Another query compares the 100 unsent keys with the worker queue. The measurement owner checks the pending group's age against documented processing expectations. The 90 rejections are grouped by supported reasons, without assuming each warning is a different failed event.

Executable JavaScript — reconcile a frozen snapshot

The following standalone example runs without dependencies in Node.js; it was tested with Node 24.14.0. It consumes already-normalised snapshots, not raw Google or Stripe responses. Repeated identical rows are harmless, out-of-cohort keys fail, and conflicting current states fail closed. A pending-to-accepted history must be reduced upstream to one evidence-backed current state before calling it. Arrival order never decides which row wins.

function reconcileSnapshot(eligibleKeys, submittedKeys, stateRows) {
  const keyOK = key => typeof key === "string" &&
    key.length > 0 && key.length <= 200 && key.trim() === key;
  function keySet(rows, label) {
    if (!Array.isArray(rows) || Array.from(rows).some(key => !keyOK(key))) {
      throw new Error("Invalid " + label + " keys");
    }
    return new Set(rows);
  }
  const eligible = keySet(eligibleKeys, "eligible");
  const submitted = keySet(submittedKeys, "submitted");
  for (const key of submitted) {
    if (!eligible.has(key)) throw new Error("Submission outside cohort");
  }
  if (!Array.isArray(stateRows)) throw new Error("State rows required");
  const current = new Map();
  for (const row of stateRows) {
    if (!row || !keyOK(row.key) || !submitted.has(row.key) ||
        !["accepted", "rejected", "pending"].includes(row.state)) {
      throw new Error("Invalid or out-of-scope state row");
    }
    if (current.has(row.key) && current.get(row.key) !== row.state) {
      throw new Error("Conflicting current states: review source evidence");
    }
    current.set(row.key, row.state);
  }
  const counts = { eligible: eligible.size, submitted: submitted.size,
    accepted: 0, rejected: 0, pending: 0 };
  for (const state of current.values()) counts[state]++;
  const notSubmitted = counts.eligible - counts.submitted;
  const unclassified = counts.submitted - current.size;
  const ratio = (a, b) => b === 0 ? null : a / b;
  return { counts, notSubmitted, unclassified,
    unresolved: notSubmitted + counts.pending + unclassified,
    acceptedCoverage: ratio(counts.accepted, counts.eligible),
    terminalAcceptance: ratio(counts.accepted, counts.accepted + counts.rejected) };
}

// Synthetic keys only. State rows are a reviewed snapshot, not a history.
const eligible = Array.from({ length: 1000 }, (_, i) => "event-" + i);
const submitted = eligible.slice(0, 900);
const states = submitted.slice(0, 870).map((key, i) => ({ key,
  state: i < 720 ? "accepted" : i < 810 ? "rejected" : "pending" }));
const example = reconcileSnapshot(eligible, submitted, states);
console.log(example);
// notSubmitted: 100; unclassified: 30; unresolved: 190
// acceptedCoverage: 0.72; terminalAcceptance: 0.8888888888888888

The deduplication here applies to the canonical keys in this snapshot. It cannot deduplicate two different keys that accidentally describe the same business event. Validate the upstream identity contract independently. Nor can it resolve a legitimate correction, refund or revised business event: those need a versioned domain policy rather than a generic last-write-wins rule.

Stripe documents duplicate webhook delivery and warns that event arrival order is not guaranteed. Those facts motivate explicit upstream identity and ordering policies if webhooks feed your CRM or warehouse. They do not specify Google's ingestion contract and should not be transplanted into a Google retry policy. Stripe webhook documentation.

Tests that protect the interpretation, not only the sums

Begin with the known-answer fixture above, then duplicate every input row and confirm that counts do not change. Reverse the row order and confirm the result is identical. Add a second current state for an existing key and expect an error. Add a state for an event never submitted and expect an error. Empty arrays must return zero counts and null ratios, not a green health badge.

Test the separate numeric calculator with empty strings, null, missing values, negative counts, fractions, NaN, infinity and counts above its one-billion-event bound. Assert S ≤ E and A+R+P ≤ S before producing any result. Across valid generated fixtures, check that every population bucket is non-negative, the five buckets sum to E, and each defined ratio remains between zero and one. Scaling every count by an integer should preserve ratios and scale gaps by that integer.

These tests protect arithmetic and the declared state contract; they do not prove a production adapter correctly interprets provider responses. Add adapter-specific fixtures from approved, sanitised examples, including partial results, warning-only responses, unavailable diagnostics and repeated polling. Keep the original response version and the normalisation version with those fixtures. Never promote an undocumented assumption into a silent mapping rule.

Operate the ledger without turning it into an automatic replay engine

Keep diagnosis and mutation separate. An investigation queue can identify unsent events, but replaying them may be inappropriate after an eligibility change, a destination correction or a revised deduplication window. Require a human-reviewed replay plan that identifies the exact keys, current eligibility, provider-supported retry semantics, expected duplicate handling and a bounded verification step. The example code and companion prompt authorize none of those actions.

Use two clocks operationally: event age and diagnostic observation age. A fresh cohort may naturally contain pending events. An old diagnostic snapshot may describe an integration that has already recovered. Store both timestamps and display them beside the counts. Do not silently merge today's accepted snapshot with yesterday's rejected snapshot. The resulting totals can look plausible while violating the current-state contract.

Alert routing should follow the gap. Unsent membership belongs with the queue or export owner. Missing diagnostic linkage belongs with the adapter owner. Conflicting states require evidence review. Known rejection clusters belong with whoever controls the rejected field or prerequisite. Set operational thresholds from the integration contract, business cadence and observed baseline; this guide supplies no universal acceptable rejection rate or magic incident threshold.

Tradeoffs, limitations and the next decision

A strict snapshot model intentionally refuses to resolve contradictory evidence automatically. That increases manual review compared with simply taking the last arriving row. In return, it prevents an arbitrary transport order from deciding what the business believes. For high-volume systems, implement the same invariants in durable, partitioned storage and evaluate memory, retention and access controls separately. The JavaScript example is a small reproducible reference, not a production ingestion service.

A completely reconciled delivery ledger can still coexist with poor match rates or disagreement in ad reports. Those are separate investigations with their own populations, attribution settings and maturity windows. Conversely, an incomplete ledger does not prove poor marketing performance. Its honest output is a bounded uncertainty statement and an investigation owner.

Start with the Conversion Delivery Reconciliation Framework to freeze scope and assign owners. Use the Conversion Delivery Reconciliation Calculator for the arithmetic and an exportable evidence packet. Draft the handoff with the Conversion Delivery Incident Review Prompt. If the unresolved constraint is the implementation or ownership model, discuss that measurement problem with Akshay.

Source note: Primary documents were checked on 17 September 2026. Google release dates and documentation-update dates are distinguished above. Stripe's page did not provide a publication date in the retrieved material. No search-volume dataset was available, and no SEO ranking or AI-citation outcome is promised.

View all growth marketing articles