The simple browser path does not use a RefCampaign secret key. Install the CDN script with your public site token, verify session capture in the dashboard, and optionally call `identify()` after signup or login.

The [`@refcampaign/sdk`](https://www.npmjs.com/package/@refcampaign/sdk) package exposes two clients:

* `RefCampaignBrowser` captures the affiliate session in the browser and can attach an email hash after signup or login.
* `RefCampaignServer` sends manual conversions from your backend. It requires your secret key and is not needed for the basic browser SDK or Stripe metadata handoff.

## Quickstart

<Steps>
  <Step>
    ### Add browser capture

    For no-code sites, simple websites, or SaaS frontends where you do not want another bundled dependency, paste the CDN script in your `<head>`:

    ```html
    <script src="https://sdk.refcampaign.com/v1.js?s=rcst_PUBLIC_SITE_TOKEN" async></script>
    ```

    The script captures the session from the URL, cookie, or local storage and exposes `window.RefCampaignBrowser`.
  </Step>

  <Step>
    ### Add Stripe metadata when you create payment

    Read the RefCampaign browser session and pass it to Stripe as `refcampaign_session` metadata.

    ```ts
    const sessionId = /* read _rc_sid from your request cookie or checkout payload */
    const metadata = sessionId ? { refcampaign_session: sessionId } : {}
    ```

    This Stripe attribution step does not require a RefCampaign secret key.
  </Step>
</Steps>

## Pick an install path

### CDN script tag

Use this path for Webflow, Framer, WordPress, Wix, static HTML, or any site that lets you paste custom code:

```html
<script src="https://sdk.refcampaign.com/v1.js?s=rcst_PUBLIC_SITE_TOKEN" async></script>
```

This captures clicks and sessions. Stripe conversion attribution only needs your backend to pass `refcampaign_session` metadata to Stripe.

### CDN browser + Stripe metadata handoff

This is the recommended SaaS setup: keep browser capture on the CDN and add the session id to Stripe metadata from your backend.

```ts
const sessionId = req.cookies.get('_rc_sid')?.value
const metadata = sessionId ? { refcampaign_session: sessionId } : {}
```

### Full npm

Use npm browser capture when you prefer the browser SDK in your application bundle:

```ts
import { RefCampaignBrowser } from '@refcampaign/sdk'

RefCampaignBrowser.configure({
  siteToken: 'rcst_PUBLIC_SITE_TOKEN',
})

RefCampaignBrowser.captureSession()
```

Choose one browser path only: CDN script tag or `RefCampaignBrowser.captureSession()` from npm. The RefCampaign secret key is required only when you wire backend manual conversions.

## Stripe attribution

### Checkout Sessions

For Stripe Checkout subscriptions, set metadata both on the Checkout Session and on `subscription_data` so recurring invoices keep the attribution:

```ts
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: Request) {
  const { priceId, sessionId } = await req.json()
  const metadata = sessionId ? { refcampaign_session: sessionId } : {}

  const checkout = await stripe.checkout.sessions.create({
    line_items: [{ price: priceId, quantity: 1 }],
    mode: 'subscription',
    metadata,
    subscription_data: { metadata },
    success_url: 'https://yoursite.com/success',
    cancel_url: 'https://yoursite.com/cancel',
  })

  return Response.json({ url: checkout.url })
}
```

### One-time payments

For one-time payments, put the metadata on the Payment Intent:

```ts
await stripe.paymentIntents.create({
  amount: 4900,
  currency: 'eur',
  metadata: sessionId ? { refcampaign_session: sessionId } : {},
})
```

### Direct subscriptions

For direct subscription creation, put the metadata on the Subscription:

```ts
await stripe.subscriptions.create({
  customer: customer.id,
  items: [{ price: 'price_xxx' }],
  metadata: sessionId ? { refcampaign_session: sessionId } : {},
})
```

## Identify customers

Call `identify()` after login or signup to improve attribution when cookies are lost, Safari ITP expires storage, or the customer converts on another device.

```ts
import { RefCampaignBrowser } from '@refcampaign/sdk'

RefCampaignBrowser.identify(currentUser.email)
```

The browser SDK hashes the email client-side with SHA-256 before sending it to RefCampaign.

## Manual conversion tracking

For non-Stripe payments, send the conversion from your backend:

```ts
import { RefCampaignServer } from '@refcampaign/sdk'

const rc = new RefCampaignServer(process.env.REFCAMPAIGN_SECRET_KEY!, {
  onError(error, context) {
    console.error('RefCampaign conversion failed', { error, ...context })
  },
})

const health = await rc.verify()
if (!health.valid) {
  console.error(health.reason)
}

const result = await rc.trackConversion({
  orderId: 'ord_42',
  sessionId,
  amount: 4900,
  currency: 'EUR',
  metadata: { payment_method: 'paypal' },
})

if (!result.success) {
  console.error(result.error)
}
```

Amounts are integer cents. `orderId` is required and acts as the idempotence key. Use `sessionId` from the browser SDK session cookie, local storage, or your own checkout payload. If your checkout already stores the partner code, send `affiliateCode`; if cookies are gone after `identify()`, send `customerEmailHash`.

The server client exposes `verify()` for health checks. `onError` receives `{ orderId, attempts, operation }`, where `operation` is either `track` or `refund`.

## Refund a conversion

When a customer is refunded, reverse the conversion and claw back the affiliate commission. Pass the same `orderId` you reported:

```ts
import { RefCampaignServer } from '@refcampaign/sdk'

const rc = new RefCampaignServer(process.env.REFCAMPAIGN_SECRET_KEY!)

// Full refund
await rc.refundConversion({ orderId: 'ord_42' })

// Partial refund of €10.00 with a reason
const result = await rc.refundConversion({
  orderId: 'ord_42',
  amount: 1000,
  reason: 'partial return',
})

if (result.alreadyRefunded) {
  // Idempotent: this conversion was already refunded.
}
```

The call is idempotent on `orderId` — a repeat refund resolves with `alreadyRefunded: true`. Only `APPROVED` conversions are refundable; partial refunds claw back commission prorata. See the [postback guide](/docs/api/integration/postback#refund-a-conversion) for the underlying REST contract.

## Staging

The browser SDK defaults to `https://app.refcampaign.com`. Override it before calling `identify()` when testing against staging:

```ts
import { RefCampaignBrowser } from '@refcampaign/sdk'

RefCampaignBrowser.configure({
  apiBase: 'https://app.test.refcampaign.com',
  siteToken: 'rcst_PUBLIC_SITE_TOKEN',
  debug: true,
})
```

For manual backend conversions, the server SDK uses `apiUrl`:

```ts
const rc = new RefCampaignServer(process.env.REFCAMPAIGN_SECRET_KEY!, {
  apiUrl: 'https://app.test.refcampaign.com',
})
```

## Pure REST alternative

If you cannot add a dependency, call the endpoints directly. See [server-to-server postback](/docs/api/integration/postback).
