Docs/Reliable Attribution

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 30-day windows, no disputes.

10 min · One-time setup · Required for accurate attribution

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.

1

Merchant clicks affiliate link

Routed to your install URL with ?ref=CODE

2

Your app carries ref through OAuth

ref rides the Shopify OAuth state

3

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 30-day last-click window: when an install appears (via Partner API sync), we look back 30 days 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)

  • ~ 30-day 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.

1
Log in to PartnerDock and go to Settings → Tracking in the left sidebar.
2
Copy the token shown under Webhook token. It looks like:
a1b2c3d4-e5f6-7890-abcd-ef1234567890
Keep this token private. Anyone with your token can send install pings as your app. If it's ever exposed, click Regenerate in Settings → Tracking and redeploy your app with the new value.

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 modelSet Install URL to…Where ref lands
Path A
App-managed (OAuth)
Your OAuth start endpoint — e.g. https://app.com/auth or /api/authYour server reads it there, carries it via OAuth state
Path B
Shopify-managed
Your app's entry / App URL — where Shopify loads your embedded appYour app reads it from that first load's URL
Our redirect target is the same for both models — it always goes to whatever Install URL you set (or the App Store if you leave it blank). The only difference is which URL you enter, because each model hands you ref at a different point. Not sure which you are? See the chooser below.
Cold clicks have no shop yet. A merchant clicking an affiliate link hasn't picked a store, so your Install URL will be hit without a 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).
No install URL? You can skip this and instead forward the 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

When your install starts, capture ref from the query string and put it in the Shopify OAuth state parameter. Shopify returns state to your callback untouched, so the code survives even though Shopify strips other query params. This is all backend — no JavaScript snippet needed.

// START of install — build the OAuth authorize request
const ref = new URL(request.url).searchParams.get('ref');   // from /r/CODE → your install URL
const state = signState({ nonce, ref });                    // your app already signs a state nonce
// ...redirect the merchant to Shopify's OAuth authorize URL with this state...
signState/verifyState are illustrative — use whatever your framework already does to create and validate the OAuth state nonce (most Shopify libraries expose this). You're just adding ref to the payload it already signs.

Path A · App-managed install — forward the code on your callback

In your OAuth callback, read ref back out of state 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 the exchange succeeds and you have the shop domain
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
  })
});
Run this after the OAuth exchange succeeds. Fire it as a non-blocking call and wrap it in a try/catch so a PartnerDock outage never breaks installs. If none of ref, mref, or the cookie is present, PartnerDock falls back to the 30-day window (manual confirm).
That completes Path A. If your app instead uses Shopify-managed installation / token exchange, ignore Steps A above and follow Path B.

Path B · Shopify-managed install (token exchange)

Embedded apps that use Shopify managed installation (and acquire tokens via token exchange through App Bridge) don't run their own OAuth redirect, so there's no state to carry ref through. The code is still available, just at a different moment: your app's first load.

Your affiliate getpartnerdock.com/r/CODE link routes the merchant to your install URL with ?ref=CODE. Read ref from the URL on that first load, and POST it to the same endpoint with the shop domain — no OAuth state needed.

// 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(() => {});
}
If your first load can't see the shop domain client-side, capture 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 Astate is guaranteed by Shopify, so ref always survives. Use Path B only when the app is genuinely managed-only.

Mantle-style direct links (mref)

If your affiliates share the direct App Store link apps.shopify.com/your-app?mref=CODE (the Mantle-style format), forward the mref code to the snippet as shown above. PartnerDock resolves it to the affiliate whose referral code matches and attributes the install exactly — no cookie required.

Migrating from Mantle? Your affiliates' existing ?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 → 30-day window (the window is a manual-confirm fallback).

Step 5 — Verify it works

1
Deploy your updated install start + callback to your staging or production environment.
2
On a dev/test store, install your app through an affiliate's getpartnerdock.com/r/CODE link so a real ref is carried end to end.
3
The referral should appear in Referrals within seconds, already attributed to that affiliate (exact, auto-approved).
4
The Reliable attribution status in Settings → Tracking flips from Not verified to Verified, and the setup banner clears.

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 the 30-day last-click window — but 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.