Skip to main content
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.
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.
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 or JavaScript tracking.

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)
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.

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.
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());
});
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 below.

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.
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');
});

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.
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": "[email protected]",
    "name": "Alex"
  }'
See Refer-a-friend 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.jspassport.authenticate('google', { state }).
  • Laravel SocialiteSocialite::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.
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 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) 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.

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.
Looking for the underlying API calls without an OAuth wrapper? See Server-to-server (S2S) tracking. For client-side tracking with cookies, see JavaScript tracking.