> ## Documentation Index
> Fetch the complete documentation index at: https://help.the-meridian.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Referral capture

Meridian can credit an [affiliate](/affiliates) for an install your app receives. The affiliate's link carries their handle as `?mref=<handle>`, and this page covers the capture half: reading that code off your landing page and parking it where the install can find it.

[Referral attribution](/sdk-attribution) covers what Meridian does with it afterwards.

<Warning>
  A link pointing straight at your Shopify App Store listing never touches your app, so there is no code to capture and **nothing is attributed**. Affiliate links must land on a page you instrument.
</Warning>

## Capture on your landing page

Call `captureReferral` in the loader of the page tracked links land on, and put the returned cookies on the response:

```ts theme={null}
export const loader = async ({ request }) => {
  const { setCookies } = await meridian.captureReferral(request)

  const headers = new Headers()
  for (const cookie of setCookies) headers.append("Set-Cookie", cookie)

  return data({ /* … */ }, { headers })
}
```

It reads `?mref=` (or `?ref=`) off the URL, and when Shopify also put `?shop=` there it hands the pairing to Meridian's server-side stash. Nothing here throws, so it is safe to await on a page whose only job is to render.

`utm_source` is not an alias: it names a marketing channel rather than an affiliate.

### A redirect route is enough

A route that captures and immediately redirects keeps the affiliate's link pointing at the App Store listing:

```ts theme={null}
export const loader = async ({ request }) => {
  const { setCookies } = await meridian.captureReferral(request)

  const headers = new Headers()
  for (const cookie of setCookies) headers.append("Set-Cookie", cookie)

  return redirect("https://apps.shopify.com/your-app", { headers })
}
```

Set the program's base link to `https://your-app.com/go`, and affiliates share `…/go?mref=<handle>`. The merchant sees one instant hop, and the cookie is set first-party on your own domain, where it survives best.

<Warning>
  The capture cookie is host-only. The route calling `captureReferral` must be served from the **same host** as your app's OAuth routes (`app.your-app.com/go`, not `www.your-app.com/go`), or the callback never sees the cookie. If your marketing site lives elsewhere, have its install link relay `?mref=` to a capture route on the app host.
</Warning>

## What the result tells you

| Field | Meaning |
| - | - |
| `setCookies` | Every `Set-Cookie` to send: the code **and** the click timestamp. Use this one |
| `code` | The captured code, or `null` |
| `clickedAt` | ISO-8601 instant of the capture, or `null` |
| `shop` | The `?shop=` domain, lower-cased, or `null` |
| `stashed` | `true` when the code also reached Meridian's server-side stash |
| `cookieSkipped` | `true` when consent was declined and no cookie was written |
| `setCookie` / `headers` | The code cookie alone. Kept for older callers |

Prefer `setCookies`. `setCookie` and `headers` predate the click timestamp and carry the code alone, so a response built from them loses the click date, and with it the [tight commission window](/sdk-attribution#why-the-click-timestamp-matters).

## Consent

The capture stores two first-party cookies on your own domain for 30 days: the affiliate's handle, and when the link was clicked. They carry no visitor identifier and support no cross-site tracking.

They are still cookies a consent banner may have to gate. Pass your banner's verdict through and nothing is written to the browser:

```ts theme={null}
const capture = await meridian.captureReferral(request, {
  consent: hasAcceptedCookies(request),
})
```

With consent declined, only Meridian's server-side stash carries the code. It is keyed by the shop domain Shopify puts in the URL on install links, so attribution still works for the normal install path. A capture returning `cookieSkipped: true` with `stashed: false` attributed nothing: there was no shop context and no cookie.

Deleting an affiliate's data is a Meridian-side operation. The SDK holds no state beyond these two cookies.

## Wiring the pieces yourself

Every helper is exported and framework-agnostic: they work on the standard `URL`, `Request` and `Headers`, and none of them throws:

| Helper | Returns |
| - | - |
| `referralCodeFromUrl(url)` | The code, `mref` before `ref`, or `null` |
| `referralFromCookie(source)` | `{ code, clickedAt }`, or `null` |
| `referralCodeFromCookie(source)` | The code alone, or `null` |
| `serializeReferralCookie(code, { maxAge? })` | A `Set-Cookie` value |
| `serializeReferralClickedAtCookie(instant, { maxAge? })` | The companion `Set-Cookie` value |
| `clearReferralCookies()` | Both clearing values |
| `shopFromUrl(url)` | The `?shop=` domain, lower-cased, or `null` |

`MERIDIAN_REFERRAL_COOKIE`, `MERIDIAN_REFERRAL_CLICKED_AT_COOKIE` and `REFERRAL_TTL_SECONDS` are exported too. The cookies must be `SameSite=None; Secure`: the embedded app is framed by `admin.shopify.com`, a cross-site context where a `Lax` cookie is never sent.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.