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:
| Match | What you pass | When to use it |
|---|---|---|
| Verified | _gt_id as Stripe metadata graytower_visitor_id | Your server creates the Checkout Session or PaymentIntent. |
| Strong | Your stable app user ID as graytower_user_id, or an exact known email | The visitor ID is unavailable but you identify signed-in users. |
| Observed | A checkout_started call with a Stripe Session or PaymentIntent ID | You 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.
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.
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,
},
});import { cookies } from "next/headers";
const cookieStore = await cookies();
const visitorId = cookieStore.get("_gt_id")?.value;
const appUserId = session.user.id;
const intent = await stripe.paymentIntents.create({
amount: 2000,
currency: "usd",
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:
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.
Stripe Payment Links
- Set the Payment Link's after-payment redirect to:
https://yourdomain.com/payment-complete?gt_session_id={CHECKOUT_SESSION_ID}- Add this opt-in helper after the main Graytower snippet. Replace the write key with the public browser key from Website → Settings → Developer.
<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>- On the completed Checkout success page, call:
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.
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.