> ## Documentation Index
> Fetch the complete documentation index at: https://docs.partnero.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Social login (OAuth) tracking

> Attribute affiliate and refer-a-friend sign-ups in Partnero when users sign in with Google, Facebook, GitHub, Apple, Microsoft, or any OAuth provider. Use the OAuth state parameter to preserve the referral key across the redirect.

To attribute sign-ups in Partnero when a user signs in with Google, Facebook, GitHub, Apple, or any other OAuth provider, **pass the Partnero referral key through the OAuth `state` parameter**. The provider echoes `state` back to your callback unchanged, so the referral key survives the round-trip to the identity provider and back — without cookies, tracking pixels, or anything ad-blockers can strip.

<Info>
  **TL;DR** — Pack the Partnero referral key into the OAuth `state` value alongside a CSRF token. On the callback, decode `state` and call `POST /v1/customers` with `partner.key` (affiliate) or `referring_customer.key` (refer-a-friend). Works with every OAuth 2.0 / OIDC provider: Google, Facebook (Meta), GitHub, Apple, Microsoft (Azure AD), Auth0, Clerk, Supabase, and more.
</Info>

<Note>
  This guide assumes you already capture the referral key on the landing page (e.g. from `?aff=KEY`). If not, start with [Server-to-server (S2S) tracking → Step 1](/guides/tracking/server-to-server#step-1-capture-the-referral-key) or [JavaScript tracking](/guides/tracking/javascript-tracking).
</Note>

## Why social login breaks referral tracking

A standard referral flow attributes sign-ups using a URL parameter (`?aff=KEY`) or a first-party cookie. Social login disrupts both because the browser leaves your site for the identity provider before the user account is created.

* **URL parameters are lost.** After "Sign in with Google", the browser ends up at `yoursite.com/auth/callback?code=…&state=…` — a different URL from the landing page that carried `?aff=KEY`.
* **Cookies can be missing.** Same-site first-party cookies usually survive a Google or Facebook redirect, but they commonly fail in three cases:
  * **In-app browsers** (Instagram, TikTok, LinkedIn, Facebook) often partition or block storage.
  * **Apple Sign-In with `response_mode=form_post`** posts a cross-site `POST` to your callback. Browsers do **not** send `SameSite=Lax` cookies on cross-site POSTs.
  * **Private / incognito sessions** clear cookies between visits.

The result: a visitor who clicked your partner's link can sign up via Google in seconds — and your tracking misses the referral entirely.

## How the OAuth state parameter preserves the referral key

The `state` parameter is a built-in OAuth 2.0 / OIDC field that the provider returns to your callback unchanged. By packing the Partnero referral key into `state` (alongside a CSRF token), you guarantee it survives the redirect — independent of cookies, browser policy, or tracking blockers.

1. **Visitor lands** on `yoursite.com?aff=REFERRAL_KEY`. You store the key in their session (or read it again when they click the social-login button).
2. **Visitor clicks "Sign in with Google"** (or any provider). You generate a `state` value containing a CSRF token **and** the referral key, then redirect to the provider.
3. **Provider redirects back** to your callback URL with the same `state` value.
4. **Your callback** validates the CSRF token, extracts the referral key, creates the user, and calls `POST /v1/customers` with `partner.key` (or `referring_customer.key`).

```
landing  ──aff=KEY──▶  your site  ──state(csrf+aff)──▶  Google
                                                            │
       Partnero  ◀── POST /v1/customers ── your callback ◀──┘
                       (with partner.key)   (decode state)
```

<Warning>
  The `state` parameter is for short-lived, redirect-bound data — not secrets. Always pair the referral key with a CSRF token stored in a server session (or signed cookie) so a third party cannot forge a callback.
</Warning>

## Step 1: Build the OAuth state value

When the user clicks the social-login button, combine a CSRF token and the referral key into a single opaque string. JSON + base64url is the simplest format and is supported by every OAuth library.

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    import crypto from 'crypto';

    app.get('/auth/google', (req, res) => {
      const csrf = crypto.randomBytes(16).toString('hex');
      req.session.oauthCsrf = csrf;

      const state = Buffer
        .from(JSON.stringify({ csrf, aff: req.session.affiliateKey ?? null }))
        .toString('base64url');

      const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
      url.searchParams.set('client_id', process.env.GOOGLE_CLIENT_ID);
      url.searchParams.set('redirect_uri', 'https://yoursite.com/auth/google/callback');
      url.searchParams.set('response_type', 'code');
      url.searchParams.set('scope', 'openid email profile');
      url.searchParams.set('state', state);

      res.redirect(url.toString());
    });
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    session_start();

    $csrf = bin2hex(random_bytes(16));
    $_SESSION['oauth_csrf'] = $csrf;

    $state = rtrim(strtr(base64_encode(json_encode([
        'csrf' => $csrf,
        'aff'  => $_SESSION['affiliate_key'] ?? null,
    ])), '+/', '-_'), '=');

    $params = http_build_query([
        'client_id'     => getenv('GOOGLE_CLIENT_ID'),
        'redirect_uri'  => 'https://yoursite.com/auth/google/callback',
        'response_type' => 'code',
        'scope'         => 'openid email profile',
        'state'         => $state,
    ]);

    header('Location: https://accounts.google.com/o/oauth2/v2/auth?' . $params);
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import base64
    import json
    import os
    import secrets
    from urllib.parse import urlencode
    from flask import session, redirect

    @app.route("/auth/google")
    def google_login():
        csrf = secrets.token_hex(16)
        session["oauth_csrf"] = csrf

        state_payload = json.dumps({"csrf": csrf, "aff": session.get("affiliate_key")})
        state = base64.urlsafe_b64encode(state_payload.encode()).rstrip(b"=").decode()

        params = urlencode({
            "client_id": os.environ["GOOGLE_CLIENT_ID"],
            "redirect_uri": "https://yoursite.com/auth/google/callback",
            "response_type": "code",
            "scope": "openid email profile",
            "state": state,
        })
        return redirect(f"https://accounts.google.com/o/oauth2/v2/auth?{params}")
    ```
  </Tab>
</Tabs>

<Tip>
  If you use an authentication library (Passport.js, Laravel Socialite, Authlib, NextAuth.js, etc.), pass the `state` value through its built-in `state` / `params` option instead of building the URL by hand. See [Provider-specific guidance](#provider-specific-guidance) below.
</Tip>

## Step 2: Handle the callback and create the customer in Partnero

When the provider redirects back, decode `state`, verify the CSRF token, and use the recovered referral key in your `POST /v1/customers` call.

### Affiliate programs

For affiliate programs, send the recovered referral key as `partner.key`.

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    app.get('/auth/google/callback', async (req, res) => {
      const { code, state } = req.query;

      const decoded = JSON.parse(Buffer.from(state, 'base64url').toString());
      if (decoded.csrf !== req.session.oauthCsrf) {
        return res.status(400).send('Invalid state');
      }
      delete req.session.oauthCsrf;

      const profile = await exchangeCodeForProfile(code);
      const user = await createOrFindUser(profile);

      await fetch('https://api.partnero.com/v1/customers', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.PARTNERO_API_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          partner: decoded.aff ? { key: decoded.aff } : undefined,
          key: user.id,
          email: user.email,
          name: profile.given_name,
          surname: profile.family_name,
        }),
      });

      res.redirect('/dashboard');
    });
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    session_start();

    $state = $_GET['state'] ?? '';
    $decoded = json_decode(base64_decode(strtr($state, '-_', '+/')), true) ?: [];

    if (($decoded['csrf'] ?? null) !== ($_SESSION['oauth_csrf'] ?? null)) {
        http_response_code(400);
        exit('Invalid state');
    }
    unset($_SESSION['oauth_csrf']);

    $profile = exchange_code_for_profile($_GET['code']);
    $user = create_or_find_user($profile);

    $body = [
        'key'     => $user->id,
        'email'   => $user->email,
        'name'    => $profile['given_name']  ?? null,
        'surname' => $profile['family_name'] ?? null,
    ];

    if (!empty($decoded['aff'])) {
        $body['partner'] = ['key' => $decoded['aff']];
    }

    $ch = curl_init('https://api.partnero.com/v1/customers');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . getenv('PARTNERO_API_KEY'),
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS     => json_encode($body),
    ]);
    curl_exec($ch);
    curl_close($ch);

    header('Location: /dashboard');
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import base64
    import json
    import os
    import requests
    from flask import request, session, redirect

    @app.route("/auth/google/callback")
    def google_callback():
        state = request.args.get("state", "")
        padded = state + "=" * (-len(state) % 4)
        decoded = json.loads(base64.urlsafe_b64decode(padded))

        if decoded.get("csrf") != session.pop("oauth_csrf", None):
            return "Invalid state", 400

        profile = exchange_code_for_profile(request.args["code"])
        user = create_or_find_user(profile)

        body = {
            "key": user.id,
            "email": user.email,
            "name": profile.get("given_name"),
            "surname": profile.get("family_name"),
        }

        if decoded.get("aff"):
            body["partner"] = {"key": decoded["aff"]}

        requests.post(
            "https://api.partnero.com/v1/customers",
            headers={
                "Authorization": f"Bearer {os.environ['PARTNERO_API_KEY']}",
                "Content-Type": "application/json",
            },
            json=body,
        )

        return redirect("/dashboard")
    ```
  </Tab>
</Tabs>

### Refer-a-friend programs

For refer-a-friend programs, send the recovered referral key as `referring_customer.key`. Omit the field entirely if `state` didn't carry a referral key.

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl --location 'https://api.partnero.com/v1/customers' \
      --header 'Authorization: Bearer YOUR_API_KEY' \
      --header 'Content-Type: application/json' \
      --data '{
        "referring_customer": { "key": "REFERRAL_KEY_FROM_STATE" },
        "key": "customer_456",
        "email": "friend@example.com",
        "name": "Alex"
      }'
    ```
  </Tab>

  <Tab title="Node.js">
    ```javascript theme={null}
    const body = {
      key: user.id,
      email: user.email,
      name: profile.given_name,
    };

    if (decoded.aff) {
      body.referring_customer = { key: decoded.aff };
    }

    await fetch('https://api.partnero.com/v1/customers', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.PARTNERO_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    $body = [
        'key'   => $user->id,
        'email' => $user->email,
        'name'  => $profile['given_name'] ?? null,
    ];

    if (!empty($decoded['aff'])) {
        $body['referring_customer'] = ['key' => $decoded['aff']];
    }

    $ch = curl_init('https://api.partnero.com/v1/customers');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . getenv('PARTNERO_API_KEY'),
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS     => json_encode($body),
    ]);
    curl_exec($ch);
    curl_close($ch);
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    body = {
        "key": user.id,
        "email": user.email,
        "name": profile.get("given_name"),
    }

    if decoded.get("aff"):
        body["referring_customer"] = {"key": decoded["aff"]}

    requests.post(
        "https://api.partnero.com/v1/customers",
        headers={
            "Authorization": f"Bearer {os.environ['PARTNERO_API_KEY']}",
            "Content-Type": "application/json",
        },
        json=body,
    )
    ```
  </Tab>
</Tabs>

See [Refer-a-friend API integration](/guides/referral-programs/api-integration) for the full request body, stats, balance, and reward endpoints.

## Provider-specific guidance

The same pattern works for every OAuth 2.0 / OIDC identity provider — only the authorisation URL and library calls change. Below are notes for the most common providers used with Partnero.

### Google (Sign in with Google)

* Authorisation URL: `https://accounts.google.com/o/oauth2/v2/auth`
* Pass `state=<your-encoded-value>` in the query string. Google returns it unchanged to your `redirect_uri`.
* Works with Passport.js (`passport-google-oauth20`), NextAuth.js / Auth.js, Laravel Socialite, Authlib, Firebase Auth, and Supabase Auth.

### Facebook / Meta

* Authorisation URL: `https://www.facebook.com/v18.0/dialog/oauth`
* `state` is round-tripped on the callback. Be aware that Facebook's in-app browser (and Instagram's) can drop cookies between landing and callback — the `state` approach avoids the problem entirely.

### GitHub

* Authorisation URL: `https://github.com/login/oauth/authorize`
* GitHub strongly recommends `state` for CSRF protection and echoes it on the callback — perfect for piggybacking the Partnero referral key.

### Apple (Sign in with Apple)

* Authorisation URL: `https://appleid.apple.com/auth/authorize`
* Apple supports `state` and, when you request `scope=name email`, sends the callback as `response_mode=form_post` (a cross-site `POST` to your `redirect_uri`). Cookies with `SameSite=Lax` are **not** sent on cross-site POSTs, so the `state` parameter is the only reliable carrier for the referral key with Apple sign-in.

### Microsoft (Azure AD / Entra ID)

* Authorisation URL: `https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize`
* Full OIDC support; `state` is up to 2 KB and returned unchanged.

### LinkedIn, Twitter/X, Discord, Slack, and others

* All standard OAuth 2.0 providers support `state`. Use the same pattern: pack the Partnero referral key, redirect, decode on callback.

### Auth library hints

* **NextAuth.js (Auth.js)** — add the referral key inside `authorization.params.state`, or store it on the `Account` row during the `signIn` event.
* **Passport.js** — `passport.authenticate('google', { state })`.
* **Laravel Socialite** — `Socialite::driver('google')->with(['state' => $state])->redirect();`
* **Authlib (Python)** — pass `state` to `authorize_redirect()`.
* **Auth0** — use `appState` in `loginWithRedirect`, read it from the `appState` argument in `onRedirectCallback`.
* **Clerk** — pass `redirectUrlComplete` with the key as a query string, or use `unsafeMetadata` on `signUp`.
* **Supabase Auth** — include the key in `queryParams` of `signInWithOAuth`, read it from the post-callback URL.
* **Firebase Authentication** — use `signInWithRedirect` and persist the referral key in your own session before the redirect; read it back when `getRedirectResult()` resolves.

## Fallback: first-party cookie

If you can't (or don't want to) modify the OAuth `state`, set a first-party cookie on landing and read it from the callback request.

1. On landing with `?aff=KEY`, set a `HttpOnly` cookie: `aff_key=KEY; Max-Age=2592000; Path=/; SameSite=Lax`.
2. After the OAuth callback completes and the user is created, read the cookie from the request and call `POST /v1/customers` with `partner.key`.

The cookie approach is simpler but more fragile than `state`:

* **Safari ITP** caps `document.cookie`-set cookies at 7 days. (Server-set cookies are unaffected.)
* **In-app browsers** may partition cookies away from your domain entirely.
* **Apple Sign-In** with `response_mode=form_post` (cross-site POST callback) won't send `SameSite=Lax` cookies.

Prefer `state`; use cookies only when your OAuth library makes injecting `state` awkward.

## Already using PartneroJS?

If you have [PartneroJS](/guides/tracking/javascript-tracking) installed, it sets `partnero_partner` (affiliate) or `partnero_referral` (refer-a-friend) as a first-party cookie on landing. In most flows this cookie survives the OAuth round-trip, so you have two options:

* **Let PartneroJS handle it** — call `po('customers', 'signup', ...)` on the post-sign-in page. PartneroJS reads the cookie automatically and attributes the sign-up.
* **Read it server-side** — extract the cookie from the callback request and pass it as `partner.key` to `POST /v1/customers`.

For flows where the cookie can be lost (Apple Sign-In with form-post, in-app browsers, private mode), fall back to the OAuth `state` pattern above.

## Track sales after sign-up

Once the customer exists in Partnero, track purchases like any other integration — call `POST /v1/transactions` with the same customer `key`, or let a connected billing integration ([Stripe, Paddle, Chargebee](/guides/tracking/transaction-based-attribution)) do it automatically.

## Frequently asked questions

### Does this work with Sign in with Apple?

Yes — and `state` is the **only** reliable way for Apple. When Apple's `response_mode=form_post` is in play, the callback is a cross-site `POST` to your `redirect_uri`, which strips `SameSite=Lax` cookies. Encoding the Partnero referral key into `state` survives that POST.

### Can I use the same approach across multiple OAuth providers?

Yes. Generate the same `state` (CSRF + referral key) for every provider and decode it the same way in every callback. The code differs only in the authorisation URL.

### What if I'm using NextAuth.js, Auth.js, or another framework?

Use the library's built-in `state` / `params` hook to inject the referral key, then read it back inside the appropriate callback (`signIn`, `redirect`, `jwt`, or your custom callback handler) and call `POST /v1/customers` from there. See [Auth library hints](#auth-library-hints).

### Do I still need PartneroJS if I'm using social login?

No. The OAuth `state` pattern works without PartneroJS — it's pure server-side. PartneroJS is still useful if you want client-side click tracking or auto-detect form sign-ups on non-OAuth flows. The two can coexist.

### Can I attribute a sign-up that happens later (not in the OAuth callback)?

Yes. Persist the referral key in your database (e.g. on the user row) when you create the user from the OAuth callback, then send it to Partnero whenever the user's account is provisioned — even if that happens days later. Partnero attributes based on the referral key you provide, not when the customer record is created.

### What if `state` is empty or invalid?

Reject the callback (return `400`). An invalid `state` either means the user didn't start the flow on your site, or the request was tampered with. Always validate the CSRF portion before trusting the referral key.

## Checklist

1. Capture `aff` (or your program's referral parameter) on the **landing page**.
2. Encode it into the OAuth **`state`** value alongside a CSRF token before redirecting to the provider.
3. On the **callback**, validate CSRF, extract the referral key, and call `POST /v1/customers` with `partner.key` (affiliate) or `referring_customer.key` (refer-a-friend).
4. Track sales as usual via `POST /v1/transactions`, or let a connected billing integration handle it.

<Note>
  Looking for the underlying API calls without an OAuth wrapper? See [Server-to-server (S2S) tracking](/guides/tracking/server-to-server). For client-side tracking with cookies, see [JavaScript tracking](/guides/tracking/javascript-tracking).
</Note>
