Reliable Attribution
Carry the affiliate's referral code through your app's install flow so PartnerDock credits the right affiliate exactly — every time, even when Shopify strips the query string. No guessing, no last-click windows, no disputes.
10 min · One-time setup · Required for accurate attribution
On this page
How reliable attribution works
Every affiliate has a unique referral code. The goal is simple: when a merchant installs your app after following an affiliate's link, that code should reach PartnerDock so we credit the right affiliate — with certainty, not a guess.
The reliable way to do that is to carry the code through your install flow. An affiliate's getpartnerdock.com/r/CODE link routes the merchant through your install URL with ?ref=CODE, and your app forwards that code to PartnerDock along with the shop domain. We match it to the affiliate and create the referral instantly, exactly.
Exactly how you forward it depends on your app's install model — whether you run OAuth yourself (authorization code grant) or use Shopify-managed installation with token exchange. Both are fully supported; the chooser below points you to the right path.
The same endpoint also accepts mref (Mantle-style direct App Store links) and the pd_click cookie — all three (ref, mref, utm_source) resolve to the same affiliate code, so existing and migrated links keep working. See Mantle-style direct links.
Merchant clicks affiliate link
Routed to your install URL with ?ref=CODE
Your app carries ref through OAuth
ref rides the Shopify OAuth state
Callback forwards ref + shop
PartnerDock matches the code exactly
What happens without it
If your app doesn't forward the code, PartnerDock can only fall back to a short last-click window: when an install appears (via Partner API sync), we look back a short window for any affiliate click for your app and credit the most recent one. But a click carries no shop identifier — it happens before the install — so this is a guess. It can credit the wrong affiliate (even a competitor's test click). For that reason these matches are never auto-approved — each one is held for you to confirm by hand in the Referrals queue.
Direct App Store links (?mref=CODE) attribute only if your app forwards the mref code — Shopify does not pass it to your OAuth callback on its own. Without forwarding, those installs get no credit at all.
In short: the fallback is a safety net, not a foundation. To run a payout program on accurate numbers, the code-carry setup below is required.
With the integration
- ✓ Exact attribution — right affiliate always credited
- ✓ Auto-approved — no manual confirmation needed
- ✓ Direct App Store
?mref=links attributed - ✓ Referral appears instantly on install
- ✓ Works even if Partner API isn't connected
Without it (fallback)
- Short last-click guess, no shop identifier
- ✗ Held for manual confirmation on every install
- ✗ Can credit the wrong affiliate (even a test click)
- ✗ Direct
?mref=links not attributed at all - ~ Referral appears only after Partner API sync
Step 1 — Get your webhook token
Your webhook token is a unique UUID that identifies your app to PartnerDock. You'll paste it directly into the snippet.
a1b2c3d4-e5f6-7890-abcd-ef1234567890
Step 2 — Set your install URL
In Settings → Tracking → Reliable attribution, enter your app's install URL — the URL that starts your install (where you build the Shopify OAuth request). Once it's set, affiliate getpartnerdock.com/r/CODE links route the merchant through that URL carrying ?ref=CODE, so the code is available to your install flow.
What exactly should the Install URL be? It's a URL you control — a different one depending on your install model:
| Your model | Set Install URL to… | Where ref lands |
|---|---|---|
| Path A App-managed (OAuth) | Your OAuth start endpoint — e.g. https://app.com/auth or /api/auth | Your server reads it there, carries it via OAuth state |
| Path B Shopify-managed | Your app's entry / App URL — where Shopify loads your embedded app | Your app reads it from that first load's URL |
ref at a different point. Not sure which you are? See the chooser below.shop param. Make sure it handles that — show your normal "enter your store / install" step — while keeping ref through it (Path A does this via state; Path B via a first-party cookie, see below).mref code from your affiliates' direct App Store links (see below). Either way, your app must forward a code — that's the required part.Which installation model are you on?
How you carry ref through the install depends on how your app authenticates. There are two models — pick the one that matches your app, then follow that path. Steps 1–2 above (token + install URL) are the same for both.
Path A · App-managed install
You run OAuth yourself (the authorization code grant): the merchant hits your /auth endpoint, you build the Shopify authorize URL, and Shopify redirects back to your /auth/callback.
How to tell: you wrote an OAuth callback route and it's listed under “Allowed redirection URL(s)” in your Partner Dashboard.
→ Follow Path A (backend only, no JavaScript)
Path B · Shopify-managed install
Shopify handles installation and you acquire tokens via token exchange through App Bridge — there's no developer-controlled OAuth redirect and no custom state.
How to tell: you enabled “Shopify managed installation” and don't have your own /auth/callback; your app boots embedded via App Bridge.
→ Follow Path B (read ref on first load)
Path A · App-managed install — carry the code through OAuth
Your OAuth state is a random CSRF nonce, and it must stay that way. Keep your random nonce and your CSRF check exactly as they are — nothing here weakens them. The referral code rides alongside the nonce; it never replaces it. Shopify returns state to your callback untouched, so the code survives even though Shopify strips other query params. There are two equally safe ways to do it:
Option A — keep state a pure random nonce (most conservative). Leave your authorize URL exactly as it is: state is just your random nonce. Store the ref server-side, keyed by that nonce (you already store the nonce in order to verify it on callback). On the callback you verify the nonce as usual, then look the ref back up. Your state never contains anything but a random value, so there is zero change to your CSRF posture.
// START of install
const ref = new URL(request.url).searchParams.get('ref'); // from /r/CODE → your install URL
const nonce = randomNonce(); // your existing random CSRF nonce
await store.put(nonce, { ref }); // remember ref against the nonce (you already store the nonce)
// ...redirect to Shopify's authorize URL with state = nonce (unchanged, fully random)...Option B — include ref in the signed state payload. If you prefer not to add a lookup, pack both into state and validate only the nonce portion on callback. The nonce is still random and still verified, so CSRF protection is unchanged. This is the approach Shopify's own support describes for carrying a referral through install.
// START of install
const ref = new URL(request.url).searchParams.get('ref');
const state = signState({ nonce, ref }); // nonce stays random; ref rides alongside
// ...redirect to Shopify's authorize URL with this state...signState/verifyState/store are illustrative — use whatever your framework already does to create and validate the OAuth state nonce (most Shopify libraries expose this). Either option keeps your random nonce and CSRF validation fully intact; you are only carrying the referral code through a flow you already run.Path A · App-managed install — forward the code on your callback
In your OAuth callback, verify the nonce as you always do, recover the ref (look it up by the nonce if you used Option A, or read it out of the verified state if you used Option B), and POST it to PartnerDock with the shop domain — from your backend, right there in the callback. You already have both shop and ref at this point, so no frontend script is involved. Replace YOUR_TOKEN with the token from Step 1.
// OAuth CALLBACK — after you verify the nonce and the exchange succeeds
// Option A: const { ref } = await store.get(nonceFromState);
// Option B: const { ref } = verifyState(request.query.state);
await fetch('https://getpartnerdock.com/api/install-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: 'YOUR_TOKEN',
shop: shop, // e.g. "acme-store.myshopify.com"
ref: ref ?? null, // the deterministic affiliate code — exact attribution
click_token: req.cookies['pd_click'] ?? null, // still honoured as a fallback
mref: req.query.mref ?? null // Mantle-style direct links still honoured
})
});try/catch so a PartnerDock outage never breaks installs. If none of ref, mref, or the cookie is present, PartnerDock falls back to the short last-click window (manual confirm).Path B · Shopify-managed install (token exchange)
Embedded apps that use Shopify managed installation (tokens via token exchange through App Bridge) don't run their own OAuth redirect, so there's no state to carry ref through. Shopify has confirmed there is no supported mechanism to pass a custom value through a managed install, so the exact, guaranteed carry of Path A is not available here. There is a best-effort workaround, and a fallback.
Best-effort workaround: your affiliate getpartnerdock.com/r/CODE link routes the merchant to your install URL with ?ref=CODE. Capture ref there and persist it in a first-party cookie/session, then read it on your first authenticated load and POST it with the shop domain. This is not guaranteed, embedded apps load in an iframe where cookie access is restricted, and the merchant may install without passing through your URL first.
// First embedded load after install (App Bridge / token-exchange app)
const params = new URLSearchParams(window.location.search);
const ref = params.get('ref'); // from /r/CODE → your install URL
const shop = params.get('shop'); // provided by Shopify
if (ref && shop) {
fetch('https://getpartnerdock.com/api/install-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: 'YOUR_TOKEN', shop, ref }),
}).catch(() => {});
}ref when it's present and forward it from your server the first time you have a confirmed shop (e.g. after token exchange). The rule is the same for both install models: get the affiliate code to the endpoint alongside the shop domain — however your app's install flow works.Important caveat for Path B
For this to work, ref has to survive to your first embedded load. Shopify's managed-install handshake runs before your app boots, and if it strips the query string, ref won't reach you. It's not guaranteed the way state is in Path A. To be safe, capture ref the moment it appears at your entry URL and persist it in a first-party cookie/session, then read it from there on the first authenticated load.
If your app can run its own auth route, prefer Path A — state is guaranteed by Shopify, so ref always survives. Use Path B only when the app is genuinely managed-only.
For a genuinely managed-only app, the realistic fallback is GA4 attribution (best-effort): Shopify records the referral code on the listing page view and fires a server-side install event we can join by session, then match to your Partner API data. It misses cross-device and blocked-tracking installs, so it's surfaced for review rather than credited automatically. See GA4 setup.
Mantle-style direct links (mref)
mref is just another name for the referral code, ref, mref, and utm_source all resolve to the same affiliate. How reliably it attributes depends on how the merchant arrives:
- Through a tracked redirect (your
/r/CODElink, which routes to your install URL carrying the code): you forward it to the endpoint above and it's credited exactly, same asref. - A raw App Store
?mref=link (apps.shopify.com/your-app?mref=CODE): Shopify strips the parameter before your callback, so it never reaches your server. It can only be recovered through GA4 (best-effort, see GA4 setup), not forwarded via the snippet.
So for exact credit, point affiliates at their tracked /r/ link. Raw App Store links still attribute, just through the best-effort GA4 path.
?mref= links keep working: migration preserves each affiliate's original Mantle code as their PartnerDock referral code. Capture mref from the install request and forward it the same way you forward ref (through the OAuth state, since Shopify strips it otherwise). ref, mref, and utm_source all resolve to the same affiliate code, so you can forward whichever one is present. Matching priority is cookie → forwarded code → short last-click window (the window is a manual-confirm fallback).Signed requests (recommended, optional)
The examples above send your token in the request body. That works and stays supported. For stronger security, you can instead sign each request so your token never leaves your server. You send a signature and a timestamp header rather than the token itself. A stolen token cannot be replayed, and a request cannot be forged without the token.
Compute an HMAC-SHA256 of {timestamp}.{body} using your tracking token as the key, put your app ID (not secret, shown in Settings → Tracking) in the body, and send two headers:
import crypto from 'crypto';
// The exact body string you will send (do not re-stringify after signing).
const body = JSON.stringify({
app_id: 'YOUR_APP_ID', // from Settings > Tracking (not secret)
shop, // the .myshopify.com domain
ref: ref ?? null, // forwarded affiliate code (or mref / utm_source)
});
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
.createHmac('sha256', 'YOUR_TOKEN') // your tracking token, kept server-side, never sent
.update(`${timestamp}.${body}`)
.digest('hex');
await fetch('https://getpartnerdock.com/api/install-report', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-PartnerDock-Timestamp': timestamp,
'X-PartnerDock-Signature': signature,
},
body, // send the SAME string you signed
});body string once and reuse it. Your token is used only as the signing key and is never transmitted. Both methods are accepted, so you can switch to signing whenever it suits you, with no downtime.Step 5 — Verify it works
getpartnerdock.com/r/CODE link so a real ref is carried end to end.Framework examples
getRefFromState() is illustrative — it's wherever you read the ref you signed into the OAuth state at install start (Step 3). Use your framework's existing state verification. As a fallback the examples also read ref/mref directly from the query string.Remix (Shopify CLI)
// app/routes/auth.callback.tsx
export async function loader({ request }: LoaderFunctionArgs) {
const { session } = await shopify.authenticate.admin(request);
const shop = session.shop;
const cookieHeader = request.headers.get("Cookie") ?? "";
const cookies = Object.fromEntries(cookieHeader.split(";").map(c => {
const [k, ...v] = c.trim().split("=");
return [k, v.join("=")];
}));
// ref rides the OAuth state you signed at install start; mref/utm_source also accepted
const url = new URL(request.url);
const ref = getRefFromState(url.searchParams.get("state")) ?? url.searchParams.get("ref");
const mref = url.searchParams.get("mref");
// Non-blocking — don't await
fetch("https://getpartnerdock.com/api/install-report", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: "YOUR_TOKEN",
shop,
ref: ref ?? null,
click_token: cookies["pd_click"] ?? null,
mref: mref ?? null,
}),
}).catch(() => {});
return redirect("/app");
}Next.js (App Router)
// app/api/auth/callback/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const shop = searchParams.get("shop") ?? "";
const cookieStore = cookies();
const clickToken = cookieStore.get("pd_click")?.value ?? null;
const ref = getRefFromState(searchParams.get("state")) ?? searchParams.get("ref");
const mref = searchParams.get("mref");
// Non-blocking
fetch("https://getpartnerdock.com/api/install-report", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: "YOUR_TOKEN", shop, ref: ref ?? null, click_token: clickToken, mref: mref ?? null }),
}).catch(() => {});
// ... rest of your OAuth exchange
}Express / Node.js
// routes/auth.js
app.get('/auth/callback', async (req, res) => {
const shop = req.query.shop;
// ref rides the OAuth state you signed at install start
const ref = getRefFromState(req.query.state) ?? req.query.ref;
// Non-blocking — fire and forget
fetch('https://getpartnerdock.com/api/install-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: 'YOUR_TOKEN',
shop,
ref: ref ?? null,
click_token: req.cookies['pd_click'] ?? null,
mref: req.query.mref ?? null,
}),
}).catch(() => {});
// ... rest of your OAuth exchange
});Common questions
Will this slow down my app installs?
No. The fetch fires asynchronously and your OAuth callback doesn't wait for it. Wrap it in a try/catch and don't await it — even if PartnerDock is unreachable, your install flow completes instantly.
Does it run on every page load?
No. Unlike a frontend script that loads on every admin page, this is a single fetch that fires once per install — at the moment the merchant completes OAuth. There's no ongoing performance impact.
Does this affect my LCP or my Built for Shopify performance score?
No. Shopify's Built for Shopify LCP requirement is measured on the storefront, and this call never runs there — it's part of your install flow, not a theme or storefront script. It also runs only once (at install), only when a ref is present, and never on repeat admin or page loads, so it's outside the measured surface entirely. On the classic OAuth flow the call is server-side in your callback, so there's no browser involvement at all. On managed installation / token exchange it's a single non-blocking, fire-and-forget fetch that doesn't block rendering or the main thread — and if you want zero client footprint, pass ref to your backend and fire it server-side. Either way, no impact on LCP or your BFS score.
Why carry ref through the OAuth state instead of just reading the query string?
Shopify strips arbitrary query params (like ref or mref) before it redirects to your OAuth callback — but it returns the state parameter to you untouched. Riding the code inside state is what makes it survive the install, which is why this is the reliable method.
What if no code is present at install?
Pass null for ref, mref, and click_token. PartnerDock then falls back to a short last-click window. Because a click carries no shop identifier, that match is a guess: it's never auto-approved and is held for you to confirm in the Referrals queue.
Can I add it after launch? Will I lose historical data?
Yes — add it any time. Historical referrals already in PartnerDock are not affected. The snippet only improves attribution for installs that happen after you deploy it.
Does it work for reinstalls?
Yes. If a merchant reinstalls your app after following an affiliate link, the code is carried again at reinstall time and the referral is attributed.
My app uses a different language (PHP, Ruby, Python). Can I still use it?
Yes — the endpoint accepts any standard HTTP POST with a JSON body. Carry the ref through your OAuth state, then POST to https://getpartnerdock.com/api/install-report with token, shop, and ref (and optionally mref/click_token). ref, mref, and utm_source all resolve to the same affiliate code.
Something not working? support@getpartnerdock.com
