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

# Tracking and attribution

> Install the Rally snippet, report signups and sales, and connect Stripe so creators are credited for the customers they bring. Everything you need to set it up without leaving this page.

Rally pays creators for the customers they bring. For that, a referral has to survive the trip from a creator's link to your signup and your billing. Three pieces make it work; the first is required, the other two depend on how you take payment.

```mermaid theme={null}
flowchart LR
  A["Creator's link"] --> B["Rally redirect\nrecords the click"]
  B --> C["Your site\nrally.js stores the referral"]
  C --> D["Signup\nrally.lead() or POST /v1/track/lead"]
  D --> E["Sale\nStripe webhook or POST /v1/track/sale"]
  E --> F["Commission in the\ncreator's ledger"]
```

<Tip>
  **Settings → Tracking** in Rally writes all of this for your stack, with your keys filled in, and a single prompt you can paste into Cursor, Claude Code or Codex to do the whole integration. This page is the same material, for reading.
</Tip>

## Before you start

<Steps>
  <Step title="Pick your stack">
    Under **Settings → Tracking → Stack**, select everything that applies: frontend (React, Next.js, plain HTML, Webflow, Framer, WordPress, Shopify), billing (Stripe Checkout, Stripe Payment Links) and backend (Node.js, Python, Go). The page then shows only the steps and code you need. Sites with no backend report signups from the browser; sites with one report them from the server.
  </Step>

  <Step title="Add your allowed hostnames">
    Under **Allowed hostnames**, add every domain where signups can be reported (`example.com`; subdomains are included). The publishable key below is public, so the hostname list is what stops another site from writing referrals into your account.
  </Step>

  <Step title="Where links send people">
    Under **Where links send people**, confirm the default landing page and add others if creators or ads should be able to send people to pricing or a feature page. Each destination carries its own UTM defaults; `{{creator}}`, `{{content}}` and `{{campaign}}` are filled in per link.
  </Step>

  <Step title="If you have a backend: create an API key">
    Click **Create** next to **Backend API key** (or use **Settings → API keys** with the `attribution:write` scope). Store it as `RALLY_API_KEY` in your server's secret store. Never put it in the client bundle.
  </Step>
</Steps>

## 1. Install the snippet

Add the tag to every page a creator's link can land on. Your tag, with your publishable key, is under **Settings → Tracking → Add the snippet**.

<CodeGroup>
  ```html HTML (before </body>) theme={null}
  <script src="https://app.runrally.io/rally.js" data-key="pk_your_key" defer></script>
  ```

  ```tsx Next.js (app/layout.tsx) theme={null}
  import Script from "next/script";

  export default function RootLayout({ children }) {
    return (
      <html>
        <body>
          {children}
          <Script src="https://app.runrally.io/rally.js" data-key="pk_your_key" strategy="afterInteractive" />
        </body>
      </html>
    );
  }
  ```
</CodeGroup>

When a visitor arrives from a creator's link (`?ref=a_…&via=CODE`, added by Rally's redirect), the snippet:

* stores `rally_ref` and `rally_via` as first-party cookies on your registrable domain (`example.com`), so `app.example.com` sees a referral that landed on `example.com`, for the program's attribution window (90 days by default);
* exposes `window.rally.ref()`, `window.rally.via()` and `window.rally.lead()`;
* rewrites any `buy.stripe.com` or `checkout.stripe.com` link on the page to carry `client_reference_id=<ref>`, so Payment Links attribute with no further work.

<AccordionGroup>
  <Accordion title="Options">
    - `data-cookie-domain="example.com"`: where the cookie is set. Rally finds your registrable domain on its own; set this only if that guess is wrong.
    - `data-cookie-days="60"`: a shorter or longer attribution window than the program's default.
  </Accordion>

  <Accordion title="Where exactly does the snippet need to be?">
    At least on the site creator links land on, usually the marketing site. Because the cookie is set on the registrable domain, an app on a subdomain reads it without its own copy of the snippet. Installing it on both is fine.
  </Accordion>

  <Accordion title="Is the publishable key secret?">
    No. It identifies your workspace and is safe in the browser. Anything that writes is also checked against your allowed hostnames. You can rotate it under **Settings → Tracking → Rotate key**; the old key stops working at once, so update the tag at the same time.
  </Accordion>
</AccordionGroup>

## 2. Report signups

A signup (a **lead**) ties your own stable user id to the referring creator. Report it once, when the account is created. Later sales attribute through that id on their own. Clicks need nothing: Rally records them when the visitor follows the link.

<Tabs>
  <Tab title="From the browser (no backend)">
    Right after a successful signup, on a page that knows the new user's id and is on an allowed hostname:

    ```js theme={null}
    window.rally?.lead({
      external_customer_id: user.id,   // your user/customer id (stable, not PII)
      email: user.email,               // optional; enables Stripe email matching
      event_name: "signup",            // "signup" (default), "trial_started", "activation", ...
    });
    // Resolves to { created: true, creator_id } or { skipped: "no referral on this device" }.
    ```
  </Tab>

  <Tab title="From your backend">
    When the account is created on the server, read the two cookies from the request and call Rally with your API key. Skip the call when both cookies are absent, and make it non-blocking (fire-and-forget with a short timeout).

    <CodeGroup>
      ```js Node.js theme={null}
      // Read the first-party cookies rally.js set, then bind the new user to the creator.
      const ref = cookies.get("rally_ref"); const via = cookies.get("rally_via");
      if (ref || via) {
        await fetch("https://app.runrally.io/v1/track/lead", {
          method: "POST",
          headers: { Authorization: `Bearer ${process.env.RALLY_API_KEY}`, "Content-Type": "application/json" },
          body: JSON.stringify({ external_customer_id: user.id, email: user.email, ref, via, event_name: "signup" }),
        });
      }
      ```

      ```python Python theme={null}
      import os, requests

      ref, via = request.cookies.get("rally_ref"), request.cookies.get("rally_via")
      if ref or via:
          requests.post("https://app.runrally.io/v1/track/lead",
              headers={"Authorization": f"Bearer {os.environ['RALLY_API_KEY']}"},
              json={"external_customer_id": user.id, "email": user.email, "ref": ref, "via": via, "event_name": "signup"},
              timeout=5)
      ```

      ```go Go theme={null}
      // Read the first-party cookies rally.js set, then bind the new user to the creator.
      var ref, via string
      if c, err := r.Cookie("rally_ref"); err == nil {
      	ref = c.Value
      }
      if c, err := r.Cookie("rally_via"); err == nil {
      	via = c.Value
      }
      if ref != "" || via != "" {
      	body, _ := json.Marshal(map[string]any{"external_customer_id": user.ID, "email": user.Email, "ref": ref, "via": via, "event_name": "signup"})
      	req, _ := http.NewRequest("POST", "https://app.runrally.io/v1/track/lead", bytes.NewReader(body))
      	req.Header.Set("Authorization", "Bearer "+os.Getenv("RALLY_API_KEY"))
      	req.Header.Set("Content-Type", "application/json")
      	if resp, err := http.DefaultClient.Do(req); err == nil {
      		resp.Body.Close() // 201 = attributed, 200 = already attributed
      	}
      }
      ```

      ```bash cURL theme={null}
      curl -X POST https://app.runrally.io/v1/track/lead \
        -H "Authorization: Bearer $RALLY_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"external_customer_id": "user_123", "email": "jane@example.com", "ref": "<rally_ref cookie>", "via": "<rally_via cookie>", "event_name": "signup"}'
      ```
    </CodeGroup>

    `201` means a new attribution; `200` means this customer was already attributed; `400` means a field is missing. If your server knows the creator directly, `creator_code` is accepted instead of `ref` and `via`.
  </Tab>
</Tabs>

## 3. Report sales

<Tabs>
  <Tab title="Stripe">
    <Steps>
      <Step title="Connect the webhook">
        **Settings → Integrations → Stripe**: copy the endpoint URL Rally shows into your Stripe dashboard (Developers → Webhooks → Add endpoint), subscribe to the events Rally lists there (`customer.created`, `customer.updated`, `checkout.session.completed`, `customer.subscription.*`, `invoice.paid`, `invoice.payment_failed`, `charge.refunded`, `charge.dispute.*`), and paste the signing secret back into Rally. **Settings → Tracking → Report sales** shows **Connected** once it is.
      </Step>

      <Step title="Payment Links">
        Nothing to do. The snippet adds `client_reference_id` to every Payment Link on the page when the visitor came from a creator; Rally reads it from the completed session.
      </Step>

      <Step title="Checkout Sessions you create server-side">
        Pass the referral as `client_reference_id`, and your user id as `creatorops_customer_id` in metadata so the sale joins the lead you reported:

        <CodeGroup>
          ```js Node.js theme={null}
          const session = await stripe.checkout.sessions.create({
            mode: "subscription",
            line_items: [{ price: "price_123", quantity: 1 }],
            client_reference_id: cookies.get("rally_ref") ?? undefined,       // referral id from rally.js
            metadata: { creatorops_customer_id: user.id },                      // joins to leads you reported
            success_url: "https://example.com/welcome",
            cancel_url: "https://example.com/pricing",
          });
          ```

          ```python Python theme={null}
          session = stripe.checkout.Session.create(
              mode="subscription",
              line_items=[{"price": "price_123", "quantity": 1}],
              client_reference_id=request.cookies.get("rally_ref"),      # referral id from rally.js
              metadata={"creatorops_customer_id": user.id},              # joins to leads you reported
              success_url="https://example.com/welcome",
              cancel_url="https://example.com/pricing",
          )
          ```

          ```go Go theme={null}
          params := &stripe.CheckoutSessionParams{
          	Mode:       stripe.String(string(stripe.CheckoutSessionModeSubscription)),
          	LineItems:  []*stripe.CheckoutSessionLineItemParams{{Price: stripe.String("price_123"), Quantity: stripe.Int64(1)}},
          	SuccessURL: stripe.String("https://example.com/welcome"),
          	CancelURL:  stripe.String("https://example.com/pricing"),
          }
          if c, err := r.Cookie("rally_ref"); err == nil { // referral id from rally.js
          	params.ClientReferenceID = stripe.String(c.Value)
          }
          params.AddMetadata("creatorops_customer_id", user.ID) // joins to leads you reported
          sess, err := session.New(params)
          ```
        </CodeGroup>
      </Step>

      <Step title="Customers you create yourself">
        Tag them so later invoices attribute:

        <CodeGroup>
          ```js Node.js theme={null}
          const customer = await stripe.customers.create({
            email: user.email,
            metadata: {
              creatorops_ref: cookies.get("rally_ref") ?? "",   // referral id (a_…), or a creator code
              creatorops_customer_id: user.id,                  // your user id; joins to /v1/track/lead
            },
          });
          ```

          ```python Python theme={null}
          customer = stripe.Customer.create(
              email=user.email,
              metadata={
                  "creatorops_ref": request.cookies.get("rally_ref", ""),  # referral id (a_…), or a creator code
                  "creatorops_customer_id": user.id,                        # your user id; joins to /v1/track/lead
              },
          )
          ```

          ```go Go theme={null}
          params := &stripe.CustomerParams{Email: stripe.String(user.Email)}
          if c, err := r.Cookie("rally_ref"); err == nil {
          	params.AddMetadata("creatorops_ref", c.Value) // referral id (a_…), or a creator code
          }
          params.AddMetadata("creatorops_customer_id", user.ID) // your user id; joins to /v1/track/lead
          cust, err := customer.New(params)
          ```
        </CodeGroup>
      </Step>
    </Steps>

    How Rally attributes a Stripe customer, in order: `creatorops_ref` metadata (a referral id or a creator code), `client_reference_id` on the Checkout Session, `creatorops_customer_id` metadata (joined to a lead), then email match when the program allows it. Revenue comes from each paid invoice (subscriptions and invoiced charges) and, for one-time Checkout payments, from the completed session itself. Set `customer_creation: "always"` on one-time Checkouts so repeat buyers share one customer record. Whether one-time payments earn a commission is a program setting (revenue share base: all paid vs subscriptions only).
  </Tab>

  <Tab title="Any other billing">
    When money is collected, post the sale from your backend. `external_id` is your invoice or charge id and makes retries safe; amounts are in minor units (cents); set `is_subscription` for recurring plans.

    <CodeGroup>
      ```js Node.js theme={null}
      await fetch("https://app.runrally.io/v1/track/sale", {
        method: "POST",
        headers: { Authorization: `Bearer ${process.env.RALLY_API_KEY}`, "Content-Type": "application/json" },
        body: JSON.stringify({ external_id: "inv_123", external_customer_id: "user_123", amount: 4900, currency: "USD", is_subscription: true }),
      });
      ```

      ```python Python theme={null}
      requests.post("https://app.runrally.io/v1/track/sale",
          headers={"Authorization": f"Bearer {os.environ['RALLY_API_KEY']}"},
          json={"external_id": "inv_123", "external_customer_id": "user_123", "amount": 4900, "currency": "USD", "is_subscription": True}, timeout=5)
      ```

      ```go Go theme={null}
      body, _ := json.Marshal(map[string]any{"external_id": "inv_123", "external_customer_id": "user_123", "amount": 4900, "currency": "USD", "is_subscription": true})
      req, _ := http.NewRequest("POST", "https://app.runrally.io/v1/track/sale", bytes.NewReader(body))
      req.Header.Set("Authorization", "Bearer "+os.Getenv("RALLY_API_KEY"))
      req.Header.Set("Content-Type", "application/json")
      if resp, err := http.DefaultClient.Do(req); err == nil {
      	resp.Body.Close()
      }
      ```

      ```bash cURL theme={null}
      curl -X POST https://app.runrally.io/v1/track/sale \
        -H "Authorization: Bearer $RALLY_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"external_id": "inv_123", "external_customer_id": "user_123", "amount": 4900, "currency": "USD", "is_subscription": true}'
      ```
    </CodeGroup>

    If no creator is linked to that customer the response carries a warning, so wiring problems show up while you integrate rather than at payout time.
  </Tab>
</Tabs>

<Note>
  Use the same stable id everywhere: `external_customer_id` in the lead call and `creatorops_customer_id` in Stripe. That id is how Rally joins the signup to the payment.
</Note>

## 4. Verify

Under **Settings → Tracking → Verify installation**, pick a hostname and click **Verify installation**. Rally fetches the page and checks the tag is there with your key. The four signals below it fill in as real traffic arrives:

| Signal | What it means |
| - | - |
| Last verified | Rally found the snippet on the page. |
| Last landing with a referral | The snippet saw a real referred visit. Open one of your creators' links yourself to test. |
| Leads from your backend | Your lead calls are arriving. |
| Sales | A sale was attributed, from Stripe or your backend. |

Quick local checks while developing: load your site with `?ref=a_test1234&via=TEST` and confirm `document.cookie` contains `rally_ref` (a test ref proves the snippet runs; it does not attribute anyone). Confirm the lead call fires once per new account and is skipped without cookies.

## Using a coding agent

**Settings → Tracking → Coding agent prompt** is one prompt for the whole setup, tailored to your stack, that a coding agent working in your repo can follow end to end. Create the backend key first and the prompt carries it as `RALLY_API_KEY` for the agent to put in your secret store. If you have Rally's [MCP](/mcp/overview) connected, the agent can fetch the same guide itself.


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