Search documentation

Search titles, headings, and page summaries.

Improve payment matching

Pass visitor or user context into Stripe so more payments can be linked to tracked visits.

Graytower tries three matching paths, in order. Use the strongest one your checkout supports:

MatchWhat you passWhen to use it
Verified_gt_id as Stripe metadata graytower_visitor_idYour server creates the Checkout Session or PaymentIntent.
StrongYour stable app user ID as graytower_user_id, or an exact known emailThe visitor ID is unavailable but you identify signed-in users.
ObservedA checkout_started call with a Stripe Session or PaymentIntent IDYou cannot set the metadata above.

A payment match needs a recorded visit before Graytower can assign an acquisition source. Prefer an immutable user ID; email-only identity cannot safely distinguish an address change from an account switch. Call reset before another account uses the same browser.

Identify after login

Call identify after sign-in and on every authenticated app bootstrap. Use the same immutable user ID as graytower_user_id in Stripe metadata.

JavaScript
window.graytower("identify", {
  userId: user.id,
  email: user.email,
});

// Before logout or switching accounts:
window.graytower("reset");

See Identify users.

Put the visitor ID in Stripe metadata

On your server-side checkout endpoint, read the first-party _gt_id cookie. Copy its value into graytower_visitor_id on the Stripe Checkout Session or PaymentIntent. This is the strongest link. If you have an immutable app user ID, also set graytower_user_id.

TypeScript
import { cookies } from "next/headers";

const cookieStore = await cookies();
const visitorId = cookieStore.get("_gt_id")?.value;
const appUserId = session.user.id;

const checkout = await stripe.checkout.sessions.create({
  line_items: [/* ... */],
  mode: "payment",
  metadata: {
    ...(visitorId ? { graytower_visitor_id: visitorId } : {}),
    graytower_user_id: appUserId,
  },
});

Add the metadata to the Stripe object your integration already creates. Do not replace its price, success URL, or other required settings.

When metadata is unavailable

For a checkout whose Stripe ID is available in the browser, send the reserved call once per attempt:

JavaScript
const session = await stripe.checkout.sessions.create({
  line_items: [/* ... */],
  mode: "payment",
});

window.graytower("checkout_started", {
  checkoutSessionId: session.id,
});

// Direct PaymentIntent flows:
window.graytower("checkout_started", {
  paymentIntentId: intent.id,
});

Use exactly one Stripe ID. This records a possible link; it does not say a payment succeeded. An ordinary custom event such as initiate_checkout never matches payments.

  1. Set the Payment Link's after-payment redirect to:
Plain text
https://yourdomain.com/payment-complete?gt_session_id={CHECKOUT_SESSION_ID}
  1. Add this opt-in helper after the main Graytower snippet. Replace the write key with the public browser key from Website → Settings → Developer.
HTML
<script>
  window.graytowerStripe = window.graytowerStripe || function () {
    (window.graytowerStripe.q = window.graytowerStripe.q || []).push(arguments);
  };
</script>
<script
  defer
  src="https://graytower.app/js/stripe.js"
  data-write-key="YOUR_BROWSER_WRITE_KEY"
></script>
  1. On the completed Checkout success page, call:
JavaScript
window.graytowerStripe({
  checkoutSessionId: session.id,
  userId: user.id, // optional immutable app user ID
});

The helper reads _gt_id. It accepts only a completed Checkout Session and never overwrites conflicting Stripe metadata.

Server attribution API

Create a server API key with the Stripe attribution purpose under Website → Settings → Developer. Keep it on the server.

JavaScript
const visitorId = (await cookies()).get("_gt_id")?.value;

await fetch("https://graytower.app/api/stripe/attribution/server", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.GRAYTOWER_SERVER_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    paymentIntentId: intent.id, // or checkoutSessionId
    visitorId,
    userId: session.user.id,
  }),
});

If consent rules, browser protections, or an ad blocker prevent a deterministic match, the payment remains revenue but is honestly left unattributed.

Check matching

After a real or test checkout, confirm the payment appears in Revenue, then inspect whether it is attributed or unattributed. Coverage and match-level counts also appear under Website → Settings → Revenue. If revenue is present but the source is missing, check that the browser recorded a visit and the Stripe metadata or checkout ID matches the same visitor. See Unattributed revenue.