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

# Affiliate Program

> Enroll in the affiliate program and read your referrals, earnings, and payouts.

The affiliate program pays a **recurring commission on every payment** made by customers you refer,
for as long as they keep paying. You enroll from **Dashboard → Affiliates**, which is also where the
numbers below are rendered; these endpoints are the same data.

<Note>
  This is the general affiliate program, open to anyone with an account. It is separate from the
  [Hosting Partnership](/partners/hosting-partnership), which is a 20% rev-share arranged with
  maintainers of open-source apps we host. Don't enroll in both for the same traffic.
</Note>

## How it works

1. **Enroll.** You get a referral code, and two links built from it.
2. **Share the link.** A click is recorded against your code.
3. **They sign up through it.** The signup is linked to you as a `pending` referral.
4. **They pay.** The referral flips to `active` and you earn a commission on that payment and every
   one after it.

Commission is a percentage of what the customer actually paid, taken at your tier's rate at the
moment of payment. The rate is snapshotted onto each earning, so a later tier change doesn't rewrite
history. Each invoice earns once.

### Tiers

You start on **Starter** and move up automatically as the revenue you've referred crosses a
milestone. Reaching a tier also pays a one-off bonus.

| Tier    | Commission | Auto-upgrade at          | Upgrade bonus |
| ------- | ---------- | ------------------------ | ------------- |
| Starter | 10%        | - (you start here)       | -             |
| Partner | 15%        | \$500 referred revenue   | \$25          |
| Pro     | 20%        | \$2,000 referred revenue | \$100         |
| Elite   | 20%        | By arrangement           | \$250         |

Upgrades happen on the payment that crosses the milestone, and are never downgraded. Read the live
values from `GET /api/affiliates/dashboard` rather than hard-coding these - tiers are configurable,
and your account may be on a negotiated one.

## Enroll

```
POST /api/affiliates/enroll
```

No request body. Enrolling twice is safe - you get the existing record back rather than a second
one.

**Response: `201 Created`**

```json theme={null}
{
  "id": "aff_abc123",
  "user_id": "usr_xyz",
  "referral_code": "1a2b3c4d",
  "tier_slug": "starter",
  "status": "active",
  "total_revenue": 0,
  "total_earnings": 0,
  "total_paid_out": 0,
  "referral_count": 0,
  "created_at": "2026-02-20T10:00:00Z"
}
```

All money fields are in **cents**.

## Dashboard

```
GET /api/affiliates/dashboard
```

Everything the Affiliates page shows, in one call.

**Response: `200 OK`**

```json theme={null}
{
  "affiliate": { "...": "..." },
  "tier": { "slug": "starter", "name": "Starter", "commission_rate": 10 },
  "next_tier": { "slug": "partner", "name": "Partner", "commission_rate": 15, "milestone_amount": 50000, "bonus_amount": 2500 },
  "progress_pct": 34,
  "pending_balance": 1250,
  "referral_link": "https://app.instapods.com/join/1a2b3c4d",
  "referral_link_alt": "https://instapods.com/join/?r=1a2b3c4d",
  "visit_count": 214
}
```

| Field                | Description                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------- |
| `tier` / `next_tier` | Your current tier, and the one you're working toward. `next_tier` is absent on the top tier |
| `progress_pct`       | How far to `next_tier`'s milestone, 0-100                                                   |
| `pending_balance`    | Earned but not yet paid out, in cents                                                       |
| `referral_link`      | Send people here. Uses your custom slug if you've set one                                   |
| `referral_link_alt`  | The same attribution as a `?r=` parameter, for linking to the marketing site                |
| `visit_count`        | Clicks recorded on your links                                                               |

Returns `404` with `not enrolled` if you haven't enrolled yet. That is the check to make before
rendering anything - the same `404` comes back from `referrals`, `earnings`, `payouts` and
`settings`.

## Referrals

```
GET /api/affiliates/referrals
```

| Query Param | Type | Description       |
| ----------- | ---- | ----------------- |
| `limit`     | int  | Page size         |
| `offset`    | int  | Pagination offset |

**Response: `200 OK`**

```json theme={null}
{
  "referrals": [
    {
      "id": "ref_abc",
      "affiliate_id": "aff_abc123",
      "referred_user": "usr_def",
      "status": "active",
      "signed_up_at": "2026-02-01T10:00:00Z",
      "activated_at": "2026-02-05T10:00:00Z",
      "email": "gr***@example.com",
      "pod_count": 2,
      "plan_slug": "build",
      "subscription_status": "active"
    }
  ],
  "total": 12,
  "limit": 25,
  "offset": 0
}
```

**Referral statuses:** `pending` (signed up, hasn't paid), `active` (has paid at least once),
`churned`.

Emails are masked. You can see that someone signed up and whether they're paying; you cannot see who
they are or reach them through us.

## Earnings

```
GET /api/affiliates/earnings
```

Same pagination. One row per commission-earning payment.

```json theme={null}
{
  "earnings": [
    {
      "id": "ern_abc",
      "affiliate_id": "aff_abc123",
      "referral_id": "ref_abc",
      "invoice_id": "inv_xyz",
      "payment_amount": 700,
      "commission_rate": 10,
      "commission_amount": 70,
      "tier_slug": "starter",
      "created_at": "2026-02-05T10:00:00Z"
    }
  ],
  "total": 34,
  "limit": 25,
  "offset": 0
}
```

`commission_rate` and `tier_slug` are recorded per earning, so this list stays accurate across tier
changes.

## Payouts

```
GET /api/affiliates/payouts
```

Same pagination. Payouts are recorded by us when money is sent; there is no endpoint to request one.

```json theme={null}
{
  "payouts": [
    {
      "id": "pay_abc",
      "affiliate_id": "aff_abc123",
      "amount": 5000,
      "method": "paypal",
      "status": "completed",
      "reference": "8XY123456",
      "note": "",
      "paid_at": "2026-02-15T10:00:00Z",
      "created_at": "2026-02-15T10:00:00Z"
    }
  ],
  "total": 2,
  "limit": 25,
  "offset": 0
}
```

## Settings

```
PUT /api/affiliates/settings
```

```json theme={null}
{
  "paypal_email": "you@example.com",
  "custom_slug": "ada"
}
```

| Field          | Type   | Description                                                                |
| -------------- | ------ | -------------------------------------------------------------------------- |
| `paypal_email` | string | Where payouts are sent. Set this before you expect one                     |
| `custom_slug`  | string | A readable alternative to your generated code, used in both referral links |

A custom slug is 3-30 characters, lowercase letters, numbers and hyphens only.

**Response: `200 OK`** - returns the updated affiliate record.

| Code  | Reason                                                     |
| ----- | ---------------------------------------------------------- |
| `400` | Slug is the wrong length or contains disallowed characters |
| `404` | Not enrolled                                               |
| `409` | Someone else already has that slug                         |

Your original `referral_code` keeps working after you set a slug.

## Recording a Visit

```
POST /api/affiliate/visit
```

Public, no auth. Records a click against a referral code - this is what our own site calls when
someone lands with a `?ref=` or `?r=` parameter, and you only need it if you're building your own
landing page in front of a referral link.

```json theme={null}
{ "code": "1a2b3c4d" }
```

Always returns `200 OK` with `{"status": "ok"}`, including for an unknown code: it's tracking, and
it never blocks the page it's called from. A click from the affiliate's own logged-in session is
silently ignored, so you can't inflate your own visit count.
