TEASEDocs
Guides

Smart Links

One link — /r/<slug> — that holds both your paid subscription and your free trial and decides per visitor which to show: a weighted split with per-country rules. Plus click, subscriber, and revenue performance.

A smart link is one trackable URL — https://yourdomain.com/r/main — that you drop into a bio, an ad, or a message. It holds both your paid subscription and your free-trial offer at once and decides per visitor which one to show (a weighted A/B split, overridable per country). TEASE records the click and later ties any subscription, rebill, tip, or PPV back to that link — so you can see exactly what each source earns.

One link — the rotation lives inside it

You don't set up a separate "paid" link and a separate "trial" link and hand out two. You create one smart link with a policy and tune the paid/trial split inside it. The individual per-arm OnlyFans links are just a pool the smart link draws its destinations from, filled by provisioning — see Where destinations come from: the link pool at the end.

You create one /r/<slug> link (usually main) with a routing policy — the paid/trial arm weights. The /api/admin/smartlinks endpoint does not touch OnlyFans.

curl -X POST https://app.tease.link/api/admin/smartlinks \
  -H "Authorization: Bearer $TEASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "yourdomain.com", "slug": "main",
        "policy": { "arms": { "paid": { "weight": 50 }, "ftl": { "weight": 50 } } } }'
const res = await fetch('https://app.tease.link/api/admin/smartlinks', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TEASE_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    domain: 'yourdomain.com',
    slug: 'main',
    policy: { arms: { paid: { weight: 50 }, ftl: { weight: 50 } } },
  }),
});
const link = await res.json();
res = requests.post(
    "https://app.tease.link/api/admin/smartlinks",
    headers={"Authorization": f"Bearer {os.environ['TEASE_API_KEY']}"},
    json={
        "domain": "yourdomain.com",
        "slug": "main",
        "policy": {"arms": {"paid": {"weight": 50}, "ftl": {"weight": 50}}},
    },
)
link = res.json()

Parameters

FieldTypeRequiredDescription
domainstringyesYour host (must already be registered).
slugstringyes1–32 chars [a-z0-9_-]. paid and ftl are reserved. Usually main.
namestringnoA label to recognize it by.
policyobjectnoThe routing policy (arm weights + per-country rules). Defaults to 50/50, with the trial arm served only where a country has its own permanent trial link.

The public address is https://yourdomain.com/r/main. Share it as-is — TEASE takes care of routing the fan to the right offer and capturing the click server-side.

You can pin a single arm with https://yourdomain.com/r/main/paid or /r/main/ftl — for when a specific placement should always lead to a specific offer (the plain /r/main link keeps splitting traffic by the policy).

The old /r/paid and /r/ftl addresses are retired. They used to be two separate links — one always paid, one always trial. The offer is the smartlink policy's call now, not the URL's. An old link you already posted keeps working: your domain's smartlink serves it exactly like /r/main. But you cannot create new ones, and /r/ftl no longer guarantees a trial.

How it decides: the policy

The policy is what turns one link into a smart one:

  • Weighted split. Each arm gets a weight (50/50, 30/70). The choice is sticky — a returning visitor always gets the same arm.
  • Per-country rules. Override the split for specific countries (DE/AT: mostly trial, RU: paid only). By default the trial arm is served only to countries that have their own permanent trial link — everyone else lands on the paid page automatically.
  • Policy shape. {"arms": {"paid": {"weight": 50}, "ftl": {"weight": 50}}, "geo": [{"countries": ["DE"], "arms": {"ftl": {"weight": 70}, "paid": {"weight": 30}}}]}. Full shape in the API reference.

Change the split any time with PATCH /api/admin/smartlinks/{id} and a new policy — routing picks the change up within about a minute.

Destinations — where each arm points

A policy smart link routes to destination links — one per (arm, country) pair. You can view and edit them directly.

List the applied destinations (active and switched off):

curl https://app.tease.link/api/admin/smartlinks/42/inventory \
  -H "Authorization: Bearer $TEASE_API_KEY"

Add a destination for an arm and country. Leave country empty to make it the arm-wide default — the one every other country falls back to:

curl -X POST https://app.tease.link/api/admin/smartlinks/42/inventory \
  -H "Authorization: Bearer $TEASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"arm": "ftl", "country": "DE", "url": "https://onlyfans.com/yourpage/trial",
       "name": "German trial"}'

Edit one destination — swap its URL, relabel it, or toggle it in or out of routing:

curl -X PATCH https://app.tease.link/api/admin/smartlinks/42/inventory/1088 \
  -H "Authorization: Bearer $TEASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"active": false}'

Verify before launch

Before you publish, read-only endpoints show exactly what your traffic will get:

  • Used in — the buttons that point at this link, and the audiences (geo-segments) that route each country here.
  • Geo map — for every country your audience covers, the arm and the concrete destination a visitor lands on. Pass ?countries=DE,FR,US to probe specific ones.
  • Validate — a health check: it runs production routing across all bound countries and finds dead-end routes (a fan would land on nothing), enabled arms with no destination, or a link no button uses yet. Fix any block-severity issue before you go live.
curl https://app.tease.link/api/admin/smartlinks/42/validate \
  -H "Authorization: Bearer $TEASE_API_KEY"
{
  "ok": false,
  "blocking": 1,
  "warnings": 0,
  "issues": [
    {
      "severity": "block",
      "code": "dead_route",
      "message": "Audience \"German\" routes here, but country DE has no working destination — a fan would hit a dead end.",
      "fix": { "type": "country", "country": "DE" }
    }
  ]
}

Read performance

Drill into a single link to see the fans it converted and revenue broken down by conversion type. All amounts are integer net cents:

curl https://app.tease.link/api/admin/smart-links/lnk_8fq2 \
  -H "Authorization: Bearer $TEASE_API_KEY"
{
  "provider_id": "lnk_8fq2",
  "name": "instagram-bio",
  "fans": [
    { "fan_id": "f_3081", "total_net_cents": 4200, "purchases": 3, "last_at": "2026-07-01" }
  ],
  "by_type": [
    { "conversion_type": "subscription", "count": 12, "revenue_net_cents": 24000 },
    { "conversion_type": "tip", "count": 5, "revenue_net_cents": 6300 }
  ]
}
FieldDescription
fansPer-fan rollup: net cents earned, purchase count, and last activity.
by_typeCount and net revenue per conversion type (subscription, tip, PPV, rebill).

For the aggregate click → subscriber → revenue funnel across all your links, read the list endpoint — each row carries its own performance snapshot and conversion rollup.

Attach a conversion pixel

You can forward conversions to Meta or TikTok server-side so your ad platform optimizes on real revenue. See Webhooks & CAPI and Attribution.

The /r/main smart link doesn't store OnlyFans links itself — it draws them from your domain's destination pool. You fill the pool in one step by provisioning (paid + trial, per country):

curl -X POST "https://app.tease.link/api/admin/domains/yourdomain.com/provision-links" \
  -H "Authorization: Bearer $TEASE_API_KEY"

The smart link then attaches those links to arms and countries via dress_country or the destinations editor above. You never create per-arm links by hand — that's what the pool is. Hand fans only /r/main. More: Provision domain links.

Next steps

On this page