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#
- Sign up, enter your product URL and pick a reward preset.
- Paste the script tag into your marketing site and your app.
- Add a server endpoint that signs a user token with your secret key.
- Render
<reftab-tab token=…>on a “Refer a friend” page. - 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>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.
| Attribute | Description |
|---|---|
data-key | Your 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-api | Override 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.
| Claim | Required | Description |
|---|---|---|
sub | Yes | Your user id. Their referral link is tied to it. |
email | Yes | Used to match them to their Stripe customer for rewards. |
name | No | Used for their code (SARAH) and greeting. |
exp | Recommended | Expiry 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.
| Attribute | Description |
|---|---|
token | The signed user token. Changing it reloads the tab. |
program | Optional 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#
| Method | Description |
|---|---|
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.
| Resource | Permission |
|---|---|
| Customers | Write |
| Customer balance transactions | Write |
| Coupons | Write |
| Promotion codes | Write |
| Subscriptions | Write |
| Subscription schedules | Write |
| Invoices | Read |
| Charges and refunds | Read |
| Payment methods | Read |
| Webhook endpoints | Write |
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>/testHow 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. Enableallow_promotion_codesin Checkout. - By metadata:
reftab_codeorreftab_visitoron the customer or Checkout Session, or the visitor id asclient_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#
| Type | How it's applied |
|---|---|
| Free month(s) | Credit on the customer balance worth their monthly price |
| Percentage discount | Stripe coupon on their subscription |
| Fixed discount | Stripe coupon on their subscription |
| Account credit | Credit on the Stripe customer balance |
| Plan upgrade | Subscription schedule that switches plans, then switches back |
| Cash commission | Paid 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.
| Signal | What happens |
|---|---|
| Same email as the referrer | Referral blocked, no rewards |
| Same Stripe customer as the referrer | Referral blocked, no rewards |
| Same card as the referrer | Referral blocked, no rewards |
| Signed up from the referrer's network | Rewards held for your approval |
| Many signups from this referrer in a short time | Rewards held for your approval |
| Disposable email address | Rewards 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.
| Endpoint | Description |
|---|---|
POST /api/v1/user-token | Mint a one-hour tab token for a user. |
POST /api/v1/referrals | Report a signup with visitorId, clickId or code. |
GET /api/v1/referrals | List referrals. ?status=converted&limit=50 (Pro) |
POST /api/v1/referrers | Get or create a user's link and code. |
GET /api/v1/referrers | Referrers ranked by revenue. ?search= (Pro) |
GET /api/v1/rewards | List rewards. ?status=pending (Pro) |
GET /api/v1/stats | Headline numbers (Pro). |
POST /api/v1/conversions | Report 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_…"update_program and get_install_instructions, then edits your code.