and runs during HTML parse, but hubspotutk is set by js.hs-analytics.net, measured loading at ~834ms. On a first visit the cookie does not exist yet, so a single read returns nothing and the visitor gets no external ID at all — and a first visit is exactly what an ad click is. So poll briefly until HubSpot sets it. The SDK loads lazily (~4.5s) and replays a queue, so an init queued a second or two in still lands before any events are transmitted. */ (function attachExternalId() { var attempts = 0; var MAX_ATTEMPTS = 40; /* 40 x 200ms = 8s, then give up quietly */ var timer = null; function tryAttach() { var raw = getExternalId(); if (!raw) { if (++attempts >= MAX_ATTEMPTS && timer) clearInterval(timer); return; } if (timer) clearInterval(timer); sha256Hex(raw).then(function (hash) { oaiq("init", { user: { external_id_sha256: hash } }); }).catch(function () {}); } tryAttach(); /* returning visitor: immediate */ if (!getExternalId()) timer = setInterval(tryAttach, 200); })(); /* --------------------------------------------------------------------------- 2. Load the SDK off the critical path Whichever comes first: the main thread going idle after load, or the first sign of real user intent. No preconnect on purpose — a TLS handshake during LCP for a script we deliberately deferred is a net loss. Measured on clickpost.ai: load event 3614ms, SDK fetched 4508ms. ------------------------------------------------------------------------ */ var loaded = false; var INTENT = ["pointerdown", "keydown", "scroll"]; function loadSdk() { if (loaded) return; loaded = true; INTENT.forEach(function (t) { removeEventListener(t, loadSdk, { passive: true }); }); var s = document.createElement("script"); s.src = SDK; s.async = true; s.fetchPriority = "low"; document.head.appendChild(s); } INTENT.forEach(function (t) { addEventListener(t, loadSdk, { passive: true, once: true }); }); addEventListener("load", function () { window.requestIdleCallback ? requestIdleCallback(loadSdk, { timeout: 2500 }) : setTimeout(loadSdk, 1500); }, { once: true }); /* --------------------------------------------------------------------------- 3. Conversion id sha256(lowercase(email) + "|" + formId + "|" + ISO-hour), first 20 hex, "lead_" prefix. - email lowercased/trimmed so casing does not fork the id - formId keeps two different forms in the same hour from colliding - the hour bucket is only the server's fallback; the id computed here is sent to the relay verbatim, so the two paths cannot disagree even when a submit straddles an hour boundary Ads Manager's own sample omits event_id. We send it so the pixel event and the relay event collapse into one conversion instead of counting twice. ------------------------------------------------------------------------ */ function buildEventId(email, formId) { var seed = [ String(email || "").trim().toLowerCase(), String(formId || ""), new Date().toISOString().slice(0, 13) ].join("|"); return crypto.subtle.digest("SHA-256", new TextEncoder().encode(seed)).then(function (buf) { var hex = Array.prototype.map.call(new Uint8Array(buf), function (b) { return b.toString(16).padStart(2, "0"); }).join(""); return "lead_" + hex.slice(0, 20); }); } /* Guards a re-fire from the history.pushState that book-a-demo-core.js runs after submit, and from back-navigation onto the same state. */ function firedOnce(id) { try { if (sessionStorage.getItem("oaiq_" + id)) return true; sessionStorage.setItem("oaiq_" + id, "1"); return false; } catch (e) { return false; /* private mode / blocked storage: fire rather than lose it */ } } /* Path 2. keepalive lets the request finish even if the page navigates away. Failures are swallowed deliberately: a reporting call must never surface an error to someone who just submitted a form. The relay hashes the email server-side into user.emails_sha256 and adds the caller's IP and user agent, so this event carries the same match signals the pixel gets for free. */ function sendToRelay(email, formId, eventType, eventId) { try { var payload = { token: RELAY_TOKEN, email: email, form_id: formId || "", event_type: eventType, event_id: eventId, /* verbatim, so both paths always match */ source_url: location.href }; var oppref = getOppref(); if (oppref) payload.oppref = oppref; var externalId = getExternalId(); if (externalId) payload.external_id = externalId; fetch(RELAY_URL, { method: "POST", keepalive: true, headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).catch(function () {}); } catch (e) {} } /* Capture the email while the form is still on the page. onFormSubmitted does not reliably carry submissionValues, and by then the form may be removed. */ var lastEmail = ""; document.addEventListener("submit", function (e) { var f = e.target; if (!f || !f.querySelector) return; var i = f.querySelector('input[name="email"], input[type="email"]'); if (i && i.value) lastEmail = i.value; }, true); /* Every HubSpot form on the site reports through this one message event, so no module or theme JS needs editing to add coverage. */ window.addEventListener("message", function (e) { var d = e.data; if (!d || d.type !== "hsFormCallback" || d.eventName !== "onFormSubmitted") return; var email = (d.data && d.data.submissionValues && d.data.submissionValues.email) || lastEmail; if (!email) return; /* without an email there is no stable id to dedupe on */ var eventType = EVENT_BY_FORM[d.id] || EVENT_DEFAULT; buildEventId(email, d.id).then(function (id) { if (firedOnce(id)) return; oaiq("measure", eventType, { type: DATA_TYPE }, { event_id: id }); /* path 1 */ sendToRelay(email, d.id, eventType, id); /* path 2 */ }); }); })();

91ÊÓƵ

Assess Your Post-Purchase Brand Readiness Start Assessment

Retention & Growth

Turn post-purchase into a growth channel

Branded tracking pages, exchange-first returns, and personalized upsells that keep revenue in the store and bring customers back for their next order.

For Retention & Growth Teams
4.8
Rating Stars - Grey

Trusted by 450+ global brands

Where the hours go

Your ops team is running logistics on manual mode
Manual carrier picks

Manual carrier picks

Choosing the right carrier per order by hand means slower dispatch and higher shipping spend.
Exception firefighting

Exception firefighting

Delivery exceptions surface too late — usually after the customer has already complained.
Spreadsheet reconciliation

Spreadsheet reconciliation

Shipping costs, SLAs, and returns get stitched together in spreadsheets that are stale on arrival.
Returned-to-sender waste

Returned-to-sender waste

Bad addresses and failed deliveries send parcels back, doubling cost and delaying refunds.

HOW CLICKPOST HELPS

Automate the busywork, keep control of the outcome

The rules are yours. ClickPost executes them on every order in milliseconds, and flags what needs a human before it becomes a complaint.

Ship every order on the best carrier, automatically

Rules score every order on cost, speed, and on-time performance for the destination, then allocate the winning carrier from 500+ integrations.
  • 500+ carrier integrations, one API
  • Milestone alerts by email, SMS, and push
  • Estimated delivery dates that set expectations

Use case

A bad address is caught at the hub and auto-corrected before dispatch.

Business impact

Fewer failed deliveries and returned-to-sender parcels.
Frame 2147228189 (1)

Catch problems before customers do

ClickPost detects stuck, delayed, and misrouted shipments in real time, then triggers the fix, from address correction to reattempt.
  • Real-time exception detection
  • Automated address verification
  • 40% fewer returned-to-sender parcels

Use case

A bad address is caught at the hub and auto-corrected before dispatch.

Business impact

Fewer failed deliveries and returned-to-sender parcels.
Frame 2147228189

See and act on every shipment in one place

Apex unifies carrier performance, SLAs, exceptions, and returns into a single live dashboard, so ops leads decide on data.
  • Live carrier performance scorecards
  • SLA breach and cost anomaly alerts
  • Decisions on real data, not last week’s spreadsheet.

Use case

Ops leads watch carrier SLAs live and reroute before breaches happen.

Business impact

Decisions on real data, not last week’s spreadsheet.
Frame 2147228190
40%
Fewer returned-to-sender parcels
22%
Savings on logistics costs
500+
Carrier integrations
50M+
Shipments processed monthly

More on reducing support load

  • blogs
    Blogs

    Ship smarter, read first

    Practical guides on tracking, delivery, and returns from the teams solving post-purchase every day.

  • Ecommerce tools-1
    Ecommerce tools

    Free tools, real results

    Calculators, benchmarks, and utilities built to fix the post-purchase metrics that move revenue.

  • assessment
    Assessment

    Know where you stand

    Answer a few questions and see how your post-purchase experience stacks up against leading US retailers.

  • ebooks
    Ebooks

    Playbooks worth downloading

    Deep-dive guides on cutting WISMO tickets, streamlining returns, and turning one-time buyers into repeat customers.

  • ROi-calculator
    ROI calculator

    Do the Math on Growth

    See what faster returns, fewer delivery exceptions, and proactive tracking are worth to your revenue.

Post-purchase that plugs into
your existing stack

  • 600+ Carrier Partners
  • 75+ Communication Gateways
  • 50+ Storefronts, OMS, WMS
  • Shopify
  • Magento
  • Klaviyo
  • Attentive
  • Shipbob
  • Shiphero
  • UPS
  • USPS

By making post-purchase a channel: exchange-first returns keep revenue in the store, branded tracking pages upsell your highest-intent traffic, and segmented journeys bring customers back for the next order.

The return flow leads with a size swap, color change, or store credit before offering a refund. It keeps the customer and the revenue, and around 54% of returns stay as exchanges.

Instant exchange ships the replacement before the original comes back, with policy rules by product and segment deciding who qualifies. You keep the sale moving for trusted customers while your rules hold the line on the cases that need a returned item first.

Shoppers visit tracking several times per order. Putting personalized recommendations and offers on that page, on your domain, turns otherwise idle traffic into a measurable revenue line instead of a status check the carrier owns.

ClickPost reports revenue retained through exchanges, upsell from tracking recommendations, and repeat-purchase lift by segment in one view. It connects to Klaviyo and your analytics stack, so those numbers sit alongside the rest of your reporting.

Yes. Journeys, offers, and return policies key off customer value and history, so VIPs get a smoother path and at-risk buyers get win-back incentives.

Post-purchase touches every team.So does ClickPost

The same platform your support, ops, and growth teams share, each with the view and controls they need

One dashboard. Every carrier. Every shipment.

Real-time visibility across 600+ carriers, automated allocation, and exception management from a single control tower.

  • Automated carrier allocation
  • Unified exception management
  • Live multi-warehouse visibility
High Performer  Mid Market - Winter 2026
Highest User Adoption - Mid Market - Winter 2026
Momentum Leader - Winter 2026
User Loves Us
High Performer  Mid Market - Winter 2026
Highest User Adoption - Mid Market - Winter 2026
Momentum Leader - Winter 2026
User Loves Us

4.8 ★

out of 5 stars on G2.com

Users Love Us - G2 Winter 2026
Momentum Leader - Winter 2026
Highest User Adoption - Winter 2026
High Performer Mid-Market - Winter 2026
Users Love Us - G2 Winter 2026
Momentum Leader - Winter 2026
Highest User Adoption - Winter 2026
High Performer Mid-Market - Winter 2026
Users Love Us - G2 Winter 2026
Momentum Leader - Winter 2026
Highest User Adoption - Winter 2026
High Performer Mid-Market - Winter 2026
Users Love Us - G2 Winter 2026
Momentum Leader - Winter 2026
Highest User Adoption - Winter 2026
High Performer Mid-Market - Winter 2026

4.8 ★

out of 5 stars on G2.com

5B+

shipments automated per year

4.8 Ratting Stars - White
4.4 Rating Stars White 4.4

Turn Every Post-Purchase Moment
Into Revenue

Drive repeat purchases with tracking-page upsells and an exchange-first returns flow that keeps revenue in the store. Book a demo on your own numbers.
. window.disableHubSpotCookieBanner = true; function killNative() { var n = document.getElementById("hs-eu-cookie-confirmation"); if (n && n.parentNode) { n.parentNode.removeChild(n); return true; } return false; } killNative(); if (window.MutationObserver) { var mo = new MutationObserver(function () { killNative(); }); mo.observe(document.documentElement, { childList: true, subtree: true }); setTimeout(function () { mo.disconnect(); }, 15000); } var COOKIE = "cp_cookie_consent"; var EU = ["at","be","bg","hr","cy","cz","dk","ee","fi","fr","de","gr","hu","ie","it","lv","lt","lu","mt","nl","pl","pt","ro","sk","si","es","se","gb","uk","is","li","no","ch"]; function readCookie(nm) { var m = document.cookie.match(new RegExp("(?:^|; )" + nm + "=([^;]*)")); return m ? decodeURIComponent(m[1]) : null; } function writeCookie(nm, val, days) { var d = new Date(); d.setTime(d.getTime() + (days * 864e5)); document.cookie = nm + "=" + encodeURIComponent(val) + ";expires=" + d.toUTCString() + ";path=/;SameSite=Lax" + (location.protocol === "https:" ? ";Secure" : ""); } function stored() { var raw = readCookie(COOKIE); if (!raw) return null; try { var o = JSON.parse(raw); if (typeof o.analytics === "boolean") return o; } catch (e) {} return null; } function apply(state, revoke) { window._hsp = window._hsp || []; window._hsp.push(["setHubSpotConsent", { analytics: !!state.analytics, advertisement: !!state.advertisement, functionality: !!state.functionality }]); if (revoke && !state.analytics && !state.advertisement && !state.functionality) { window._hsp.push(["revokeCookieConsent"]); } window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: "cp_cookie_consent", cp_consent_analytics: !!state.analytics, cp_consent_advertisement: !!state.advertisement, cp_consent_functionality: !!state.functionality }); document.documentElement.setAttribute("data-cp-consent", "set"); } function inScope(banner) { if (banner.getAttribute("data-scope") !== "eu_only") return true; var geo = (document.body.getAttribute("data-geo_location") || "").toLowerCase(); if (!geo) return true; return EU.indexOf(geo) !== -1; } function init(banner) { var days = parseInt(banner.getAttribute("data-days"), 10) || 180; var prefs = banner.querySelector("[data-cpcb-prefs]"); var compact = banner.querySelector('[data-cpcb-actions="compact"]'); var expanded = banner.querySelector('[data-cpcb-actions="expanded"]'); var toggles = banner.querySelectorAll("[data-cpcb-toggle]"); function show() { banner.hidden = false; requestAnimationFrame(function () { banner.classList.add("is-visible"); }); } function hide() { banner.classList.remove("is-visible"); setTimeout(function () { banner.hidden = true; }, 300); } function collapse() { if (!prefs) return; prefs.hidden = true; if (expanded) expanded.hidden = true; if (compact) compact.hidden = false; } function expand() { if (!prefs) return; prefs.hidden = false; if (compact) compact.hidden = true; if (expanded) expanded.hidden = false; } function paint(state) { Array.prototype.forEach.call(toggles, function (t) { var key = t.getAttribute("data-cpcb-toggle"); if (key === "necessary") { t.setAttribute("aria-checked", "true"); return; } t.setAttribute("aria-checked", state[key] ? "true" : "false"); }); } function fromToggles() { var state = { analytics: false, advertisement: false, functionality: false }; Array.prototype.forEach.call(toggles, function (t) { var key = t.getAttribute("data-cpcb-toggle"); if (key in state) state[key] = t.getAttribute("aria-checked") === "true"; }); return state; } function commit(state, revoke) { writeCookie(COOKIE, JSON.stringify({ analytics: !!state.analytics, advertisement: !!state.advertisement, functionality: !!state.functionality, v: 1, ts: Date.now() }), days); apply(state, revoke); hide(); } Array.prototype.forEach.call(toggles, function (t) { if (t.disabled) return; t.addEventListener("click", function () { t.setAttribute("aria-checked", t.getAttribute("aria-checked") === "true" ? "false" : "true"); }); }); banner.addEventListener("click", function (e) { if (!e.target || !e.target.closest) return; var btn = e.target.closest("[data-cpcb-action]"); if (!btn) return; var action = btn.getAttribute("data-cpcb-action"); if (action === "customise") { expand(); return; } if (action === "accept") { commit({ analytics: true, advertisement: true, functionality: true }, false); return; } if (action === "reject") { commit({ analytics: false, advertisement: false, functionality: false }, true); return; } if (action === "save") { commit(fromToggles(), true); return; } }); var existing = stored(); if (existing) { apply(existing, false); paint(existing); collapse(); } else if (!inScope(banner)) { // Out of the consent-required region: implied consent, banner never shown. commitSilently(); } else { collapse(); show(); } function commitSilently() { var all = { analytics: true, advertisement: true, functionality: true }; writeCookie(COOKIE, JSON.stringify({ analytics: true, advertisement: true, functionality: true, v: 1, ts: Date.now(), implied: true }), days); apply(all, false); } // Public re-open hook for a footer "Cookie settings" link. window.cpCookieBanner = window.cpCookieBanner || {}; window.cpCookieBanner.open = function () { var s = stored() || { analytics: false, advertisement: false, functionality: false }; paint(s); expand(); show(); }; } function boot() { var banners = document.querySelectorAll("[data-cpcb]"); if (!banners.length) return; init(banners[0]); document.addEventListener("click", function (e) { if (!e.target || !e.target.closest) return; var trigger = e.target.closest("[data-cp-cookie-settings]"); if (!trigger) return; e.preventDefault(); if (window.cpCookieBanner && window.cpCookieBanner.open) window.cpCookieBanner.open(); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } })();