Documentation

Add a referral tab to your app

Three pieces: a script on every page, a signed token that says who's looking, and the <reftab-tab> element. Everything else happens in your Stripe account.

Quick start#

  1. Sign up, enter your product URL and pick a reward preset.
  2. Paste the script tag into your marketing site and your app.
  3. Add a server endpoint that signs a user token with your secret key.
  4. Render <reftab-tab token=…> on a “Refer a friend” page.
  5. Call Reftab.signup() after signup, and connect Stripe.
<script src="https://reftab.app/reftab.js" data-key="rt_pub_live_…" defer></script>

<reftab-tab token="SIGNED_USER_TOKEN"></reftab-tab>
Using Claude Code or Cursor? The Install page in your dashboard has a copy-paste prompt with your keys filled in.

The script tag#

Load reftab.js on every page, including your marketing site. When a visitor arrives with ?ref=CODE (or ?via=CODE), it records the click and stores the referral for the program's cookie window (60 days by default) in a first-party cookie and localStorage. First click wins: a later link doesn't steal the attribution. The cookie is written on your root domain, so a referral captured on acme.com is still known on app.acme.com.

AttributeDescription
data-keyYour public key: rt_pub_test_… or rt_pub_live_…. Safe to ship to browsers.
data-banner"true" shows a welcome banner to referred visitors.
data-apiOverride the API origin (self-hosting).

User tokens#

The tab needs to know which of your users is looking at it. Your backend vouches for them with a standard HS256 JWT signed with your secret key (rt_sec_…). Never expose the secret key in the browser.

ClaimRequiredDescription
subYesYour user id. Their referral link is tied to it.
emailYesUsed to match them to their Stripe customer for rewards.
nameNoUsed for their code (SARAH) and greeting.
expRecommendedExpiry as a Unix timestamp. One hour is plenty.
import { createHmac } from "node:crypto";

const b64 = (v: object) => Buffer.from(JSON.stringify(v)).toString("base64url");

export function reftabToken(user: { id: string; email: string; name?: string }) {
  const now = Math.floor(Date.now() / 1000);
  const body = `${b64({ alg: "HS256", typ: "JWT" })}.${b64({
    sub: user.id, email: user.email, name: user.name, iat: now, exp: now + 3600,
  })}`;
  const sig = createHmac("sha256", process.env.REFTAB_SECRET_KEY!).update(body).digest("base64url");
  return `${body}.${sig}`;
}

<reftab-tab> and <reftab-launcher>#

<reftab-tab> is a web component rendered in Shadow DOM, so your CSS and ours never collide. It inherits your font, takes your brand colours from the dashboard, supports light and dark mode and adapts to narrow containers. The first time a user opens it, their referral link is created.

AttributeDescription
tokenThe signed user token. Changing it reloads the tab.
programOptional program id, for projects with several programs (Pro).
theme"light", "dark" or "auto". Overrides the dashboard setting.

<reftab-launcher> adds a floating “Refer & earn” button that opens the tab in a side panel. It takes token, label, color and position (left or right).

"use client";
import { useEffect, useState } from "react";

export function ReferralTab() {
  const [token, setToken] = useState<string | null>(null);
  useEffect(() => {
    fetch("/api/reftab-token").then((r) => r.json()).then((d) => setToken(d.token));
  }, []);
  return token ? <reftab-tab token={token} /> : null;
}

declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "reftab-tab": { token?: string; program?: string; theme?: string };
      "reftab-launcher": { token?: string; label?: string; color?: string; position?: string };
    }
  }
}

The element emits a reftab:loaded event with the tab data, and exposes part="container" and part="button" for ::part() styling.

JavaScript API#

MethodDescription
Reftab.signup({ email, userId, name })Report a signup from the browser. Matches the stored click. Returns a promise of { attributed }.
Reftab.visitorId()The anonymous visitor id. Pass it to your backend or as Stripe Checkout client_reference_id.
Reftab.referral()The stored referral ({ code, clickId, … }) or null.
Reftab.ready(cb)Runs after the click is recorded. Also fires a reftab:ready event on document.
Reftab.open(token)Opens the tab in a side panel.

Browser signups are safe: rewards only happen when that email becomes a paying Stripe customer. Prefer the server? Use POST /api/v1/referrals with the visitor id.

Stripe connection#

Create a restricted key with these permissions and paste it on the Install page. Use a test key (rk_test_) for test mode and a live key for live mode.

ResourcePermission
CustomersWrite
Customer balance transactionsWrite
CouponsWrite
Promotion codesWrite
SubscriptionsWrite
Subscription schedulesWrite
InvoicesRead
Charges and refundsRead
Payment methodsRead
Webhook endpointsWrite

We register a webhook endpoint in your account for: invoice.paid, charge.refunded, customer.created, customer.subscription.updated, customer.subscription.deleted, checkout.session.completed. Developing locally? Forward events with the Stripe CLI and paste the whsec_ secret it prints:

stripe listen --forward-to https://reftab.app/api/stripe/webhook/<projectId>/test

How payments are matched

  • By the Stripe customer linked at signup (stripeCustomerId), then by email.
  • By promotion code: when the friend's reward is a discount, each referrer's code (e.g. SARAH) is also a Stripe promotion code. Enable allow_promotion_codes in Checkout.
  • By metadata: reftab_code or reftab_visitor on the customer or Checkout Session, or the visitor id as client_reference_id.

Test mode and simulator#

Every project has separate test and live data, and separate keys. In test mode without a Stripe test key, rewards run against a built-in sandbox that behaves like Stripe. The dashboard's simulator drives the real code paths: a friend clicks, signs up and pays; you can refund or cancel them, and skip the hold period to watch rewards apply.

Reward types#

TypeHow it's applied
Free month(s)Credit on the customer balance worth their monthly price
Percentage discountStripe coupon on their subscription
Fixed discountStripe coupon on their subscription
Account creditCredit on the Stripe customer balance
Plan upgradeSubscription schedule that switches plans, then switches back
Cash commissionPaid from your PayPal or Wise in a payout batch

Rewards can go to the referrer, the friend, or both. A cap per referrer is optional. Cash commissions (Pro) are a percentage of every paid invoice for a number of months, batched into PayPal Payouts or Wise batch files that you upload yourself.

Hold period and fraud rules#

Rewards are created on the friend's first paid invoice and released after the hold period (30 days by default). A refund or cancellation inside the hold period cancels them. Every reward has a status: pending, approved, applied, paid, cancelled or failed, with the reason. Switch a program to “approve manually” to review each one.

SignalWhat happens
Same email as the referrerReferral blocked, no rewards
Same Stripe customer as the referrerReferral blocked, no rewards
Same card as the referrerReferral blocked, no rewards
Signed up from the referrer's networkRewards held for your approval
Many signups from this referrer in a short timeRewards held for your approval
Disposable email addressRewards held for your approval

REST API#

Authenticate with your secret key as a Bearer token. Test and live keys see their own data. Reporting signups and payments and minting user tokens are free; listing endpoints are part of Pro.

EndpointDescription
POST /api/v1/user-tokenMint a one-hour tab token for a user.
POST /api/v1/referralsReport a signup with visitorId, clickId or code.
GET /api/v1/referralsList referrals. ?status=converted&limit=50 (Pro)
POST /api/v1/referrersGet or create a user's link and code.
GET /api/v1/referrersReferrers ranked by revenue. ?search= (Pro)
GET /api/v1/rewardsList rewards. ?status=pending (Pro)
GET /api/v1/statsHeadline numbers (Pro).
POST /api/v1/conversionsReport a payment Reftab can't see in Stripe. Idempotent on invoiceId.
curl https://reftab.app/api/v1/referrals \
  -H "Authorization: Bearer $REFTAB_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"tom@example.com","userId":"user_456","visitorId":"v_8f2c…"}'

Errors look like { "error": { "message": "…", "code": "…" } }.

Outgoing webhooks#

Set a webhook URL in Settings (Pro) and we'll POST JSON events: referral.created, referral.converted, referral.refunded, referral.churned, reward.created, reward.applied, reward.cancelled, payout.created, payout.paid, partner.joined. Each request carries a Reftab-Signature: t=…,v1=… header: an HMAC-SHA256 of `${t}.${body}` with your webhook secret, the same scheme as Stripe.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyReftab(body: string, header: string, secret: string) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret).update(`${parts.t}.${body}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}

MCP server#

Everything the dashboard does, an agent can do. The server speaks the Streamable HTTP transport at https://reftab.app/api/mcp and authenticates with your secret key (Pro). Tools include get_overview, list_referrers, list_referrals, list_rewards, get_program, update_program, approve_reward, cancel_reward, create_referral_link, get_top_sources and get_install_instructions.

claude mcp add --transport http reftab https://reftab.app/api/mcp \
  --header "Authorization: Bearer rt_sec_live_…"
Try: “Set up a give-a-month program, then add the referral tab to my settings page.” The agent calls update_program and get_install_instructions, then edits your code.