Affiliates

Run your affiliate and customer referral program from code: add partners, hand them links, record referrals from your server, sign partners in to their portal, and read the commission ledger and payouts.

Base URL: https://api.ranla.ai

Set up the program first in the dashboard (Affiliates): turn it on, connect Stripe and pick a commission. The API works on that program. For the browser side (links, checkout, signups) see the affiliate tracking guide.


Authentication

Authorization: Bearer rnl_…

Every affiliate endpoint needs a full access key. A sending key gets 403.


Resources

Resource Endpoints
Partners GET/POST /affiliates · GET/PATCH/DELETE /affiliates/{id} · POST /affiliates/{id}/approve · /reject · /suspend
Links GET/POST /affiliates/{id}/links · DELETE /affiliates/{id}/links/{link_id}
Portal sign-in (SSO) POST /affiliates/{id}/login-link
Campaigns GET/POST /affiliate-campaigns · GET/PATCH /affiliate-campaigns/{id}
Referrals GET/POST /affiliate-referrals · GET /affiliate-referrals/{id}
Commissions GET /affiliate-commissions · GET/PATCH /affiliate-commissions/{id}
Payouts GET /affiliate-payouts · GET /affiliate-payouts/{id} · POST /affiliate-payouts/{id}/mark-paid

Money is always in minor units (amount_cents), with a lowercase currency. Rates are basis points: 2000 is 20%.

Payout runs are not approved over the API. Approving a run moves money, so it stays with the account owner in the dashboard (or the approval card in Slack). The API can read payouts and mark manual ones paid once their run is approved.

Lists and pagination

List endpoints return newest first:

{ "data": [ … ], "has_more": true, "next_cursor": "eyJ0Ijoi…" }

Pass limit (1–100, default 25) and cursor=<next_cursor> for the next page. Closed-set filters take a comma-separated list: ?status=active,pending.

Errors

The standard error body. Validation errors name each bad field under details.fields:

{
  "error": {
    "message": "\"jane\" is already taken",
    "code": "conflict",
    "details": { "fields": { "token": "Already taken" } }
  }
}
Status code When
400 validation_error A field is missing or invalid, or an action does not fit the partner's status
401 unauthorized Missing or invalid key
403 forbidden Sending key, or the action is not allowed (deleting a paid partner, a primary link)
404 not_found Not in this account, or the program is not set up yet
409 conflict Email already a partner, token taken, or the row changed in the meantime

Partners

The partner object

{
  "object": "affiliate",
  "id": "5b0c…",
  "email": "[email protected]",
  "first_name": "Jane",
  "last_name": "Doe",
  "company": null,
  "website": null,
  "country": "US",
  "status": "active",
  "source": "api",
  "campaign_id": "a41e…",
  "parent_affiliate_id": null,
  "tags": ["customer"],
  "payout_method": "paypal",
  "payout_ready": true,
  "approved_at": "2026-09-25T10:00:00.000Z",
  "links": [
    {
      "object": "affiliate_link",
      "id": "9f2d…",
      "affiliate_id": "5b0c…",
      "token": "jane",
      "url": "https://yourapp.com/?via=jane",
      "destination_url": null,
      "label": null,
      "is_primary": true,
      "created_at": "2026-09-25T10:00:00.000Z"
    }
  ],
  "created_at": "2026-09-25T10:00:00.000Z",
  "updated_at": "2026-09-25T10:00:00.000Z"
}

status is one of invited, pending, active, suspended, rejected. Single-partner responses list every link; lists carry the primary link only.

Create a partner

POST /affiliates

Field Type Notes
email string Required. One partner per email.
first_name, last_name, company, website, country string Optional. country is a 2-letter code.
campaign_id string Campaign id or name. The default campaign when omitted.
token string Their link token (?via=jane). Kept exactly, or 409 when taken. Derived from the name when omitted.
status string active (default), pending (needs approval) or invited.
paypal_email string Where PayPal payouts go.
tags string[] Up to 20.
notes string Private to your team.
send_invite boolean Send the welcome email (default true). Set false when your app shows the link itself.

Customer referral programs. Turn signed-in customers into referrers from inside your app — create the partner the first time they open your "Refer a friend" page and show them their link:

import { Ranla } from '@supersend/ranla'

const client = new Ranla(process.env.RANLA_API_KEY!)

async function referralLinkFor(user: { email: string; name: string }) {
  const found = await client.affiliates.list({ email: user.email })
  const partner =
    found.data[0] ??
    (await client.affiliates.create({
      email: user.email,
      first_name: user.name,
      tags: ['customer'],
      send_invite: false,
    })).affiliate
  return partner.links?.[0]?.url
}
curl -X POST https://api.ranla.ai/affiliates \
  -H "Authorization: Bearer $RANLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","first_name":"Jane","token":"jane","send_invite":false}'

201 → { "affiliate": { … } }

List, get, update, delete

  • GET /affiliates?status=active&campaign_id=…&email=… — email is an exact lookup.
  • GET /affiliates/{id}
  • PATCH /affiliates/{id} — first_name, last_name, company, website, country, notes, tags, campaign_id (new commissions use the new campaign's rate), parent_affiliate_id, postback_url, email_opt_out, commission_override ({ "commission_type": "percent", "commission_bps": 3000 }, or null to clear), and token (renames the primary link; the old link stops attributing).
  • DELETE /affiliates/{id} — refused with 403 once the partner has been paid. Suspend them instead.

Approve, reject, suspend

curl -X POST https://api.ranla.ai/affiliates/$ID/approve \
  -H "Authorization: Bearer $RANLA_API_KEY"
Endpoint From → to Body
POST /affiliates/{id}/approve pending, invited, rejected → active send_email (default true)
POST /affiliates/{id}/reject pending, invited → rejected reason, send_email
POST /affiliates/{id}/suspend active, pending, invited → suspended reason

A suspended partner's links stop attributing and payouts skip them.

  • GET /affiliates/{id}/links
  • POST /affiliates/{id}/links — { "token": "jane-youtube", "destination_url": "/pricing", "label": "YouTube" }. destination_url is a path or a page on your website. Up to 50 links per partner.
  • DELETE /affiliates/{id}/links/{link_id} — the primary link cannot be deleted.

Sign partners in to the portal (SSO)

POST /affiliates/{id}/login-link

{ "object": "affiliate_login_link", "url": "https://acme.partners.ranla.ai/auth/callback?token=…", "expires_at": "2026-09-25T10:01:00.000Z" }

The link signs the partner in to your partner portal once and expires after one minute. Mint it when a signed-in user clicks "Referral dashboard" in your app and redirect straight away — never email or store it:

app.get('/referrals/dashboard', requireUser, async (req, res) => {
  const { url } = await client.affiliates.loginLink(req.user.affiliateId)
  res.redirect(url)
})

Rejected partners cannot sign in (403).


Campaigns

A campaign is a commission plan. Every partner belongs to one; the default campaign takes new partners.

{
  "object": "affiliate_campaign",
  "id": "a41e…",
  "name": "Standard",
  "is_default": true,
  "visibility": "public",
  "auto_approve": true,
  "commission_type": "percent",
  "commission_bps": 3000,
  "commission_flat_cents": 0,
  "commission_duration": "recurring",
  "recurring_months": 12,
  "max_commission_cents": null,
  "tiers": [{ "min_paid_customers": 10, "commission_bps": 4000, "commission_flat_cents": null }],
  "product_rates": [],
  "second_tier_bps": 0,
  "lead_bounty_cents": 0,
  "cookie_days": 60,
  "hold_days": 30,
  "archived_at": null,
  "created_at": "…",
  "updated_at": "…"
}
  • GET /affiliate-campaigns?include_archived=true — not paged; each row adds affiliate_count.
  • POST /affiliate-campaigns — name is required; pass make_default: true to make it the default.
  • GET /affiliate-campaigns/{id}, PATCH /affiliate-campaigns/{id} — new rates apply to new commissions only.

recurring_months: null pays for the customer's lifetime. hold_days is the refund window before a commission is earned. tiers raise the rate once a partner has brought that many paying customers. second_tier_bps pays a partner's recruiter a share of the same sale.


Referrals

A referral is a person a partner sent you. It starts as a lead and becomes active when they pay (trialing, cancelled, refunded and rejected follow the customer).

{
  "object": "affiliate_referral",
  "id": "c7a0…",
  "affiliate_id": "5b0c…",
  "status": "lead",
  "source": "api",
  "email": "[email protected]",
  "stripe_customer_id": "cus_Q…",
  "sub_id": null,
  "blocked_reason": null,
  "revenue_cents": 0,
  "currency": "usd",
  "converted_at": null,
  "expires_at": "2026-11-24T10:00:00.000Z",
  "created_at": "…",
  "updated_at": "…"
}

GET /affiliate-referrals filters: status, source, affiliate_id, email, created_after, created_before.

Server-side referral tracking

POST /affiliate-referrals attributes a person to a partner without the browser snippet — for example from a signup handler that received a referral code in a mobile app, an invite flow, or your own ?ref= handling.

Field Notes
affiliate_id or token Required, one of them. token is the partner's link token.
email Required. The referred person.
stripe_customer_id Optional. Ties their future Stripe payments to this referral straight away.
sub_id Optional tag for reporting.
curl -X POST https://api.ranla.ai/affiliate-referrals \
  -H "Authorization: Bearer $RANLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"token":"jane","email":"[email protected]","stripe_customer_id":"cus_Q1"}'
{ "referral": { "object": "affiliate_referral", "id": "c7a0…", "status": "lead", … }, "created": true }
  • 201 with created: true for a new referral.
  • 200 with created: false when the person was already referred — the first partner to claim a person keeps them.
  • A self-referral is recorded but flagged (blocked_reason: "self_referral") and never earns.
  • 404 when no active partner has that id or token; 409 while the program is not live.

When their Stripe customer pays, commissions are written for the partner automatically.


Commissions

One ledger line per paid invoice (and per lead bounty or recruiter share).

{
  "object": "affiliate_commission",
  "id": "e19b…",
  "affiliate_id": "5b0c…",
  "referral_id": "c7a0…",
  "tier": 1,
  "status": "pending",
  "amount_cents": 1470,
  "sale_amount_cents": 4900,
  "currency": "usd",
  "description": null,
  "stripe_invoice_id": "in_1Q…",
  "stripe_charge_id": "ch_3Q…",
  "earns_at": "2026-10-25T10:00:00.000Z",
  "earned_at": null,
  "paid_at": null,
  "reversed_at": null,
  "reverse_reason": null,
  "payout_id": null,
  "created_at": "…",
  "updated_at": "…"
}

status moves pending → earned (after the hold) → paid, or to reversed. A refund or dispute during the hold reverses it on its own.

  • GET /affiliate-commissions filters: status, affiliate_id, referral_id, currency, created_after, created_before.
  • GET /affiliate-commissions/{id}
  • PATCH /affiliate-commissions/{id} with { "status": "reversed", "reason": "fraud", "note": "Card testing" } — the only change the API makes. reason is manual (default) or fraud. Paid commissions, and commissions already in a payout, cannot be reversed (403).

Payouts

{
  "object": "affiliate_payout",
  "id": "d3f4…",
  "affiliate_id": "5b0c…",
  "run_id": "71aa…",
  "amount_cents": 9800,
  "currency": "usd",
  "method": "wise",
  "status": "processing",
  "stripe_transfer_id": null,
  "external_reference": null,
  "failure_reason": null,
  "paid_at": null,
  "created_at": "…",
  "updated_at": "…"
}
  • GET /affiliate-payouts filters: status, affiliate_id, run_id.
  • GET /affiliate-payouts/{id}
  • POST /affiliate-payouts/{id}/mark-paid with { "reference": "TX-2231" } — for paypal, wise, bank and other payouts you sent yourself. The run must already be approved (409 otherwise). Stripe payouts settle on their own (403).

Webhooks

Affiliate events go to the same webhook endpoints as email events, signed the same way. Subscribe to them by name, or leave events empty to receive everything.

Event When
affiliate.created A partner joined, was invited, imported or created over the API
affiliate.approved A partner became active
affiliate.updated A partner's profile, campaign, payout method or status changed
referral.created A new referral (lead) was recorded
referral.converted A referral became a paying customer
commission.created A commission was written (pending)
commission.earned Its hold ended
commission.reversed It was reversed (refund, dispute or by hand)
commission.paid It was paid in a payout
payout.created A payout was drafted in a run
payout.paid A payout went out
payout.failed A payout failed; its commissions return to the next run

Every payload has the same envelope. data.event_id is unique per event — use it to de-duplicate retries. data.affiliate_id names the partner, and the object the event is about rides along when there is one (affiliate, referral, commission) or by id (referral_id, commission_id, payout_id). Fetch it from the API when you need its latest state.

{
  "type": "affiliate.approved",
  "created_at": "2026-09-25T10:00:00.000Z",
  "data": {
    "event_id": "0b5e7d4c-…",
    "program_id": "3c1f…",
    "affiliate_id": "5b0c…",
    "affiliate": {
      "object": "affiliate",
      "id": "5b0c…",
      "email": "[email protected]",
      "status": "active",
      "campaign_id": "a41e…",
      "…": "…"
    }
  }
}

A common use: commission.earned to grant account credit instead of cash in a customer referral program.

Verify signatures

Exactly as for email events — read the raw body and check the SuperSendTX-Signature header with your endpoint's secret:

import { verifyWebhookSignature } from '@supersend/ranla'

const ok = verifyWebhookSignature(secret, rawBody, request.headers.get('supersendtx-signature'))
if (!ok) return new Response('Bad signature', { status: 400 })

const event = JSON.parse(rawBody)
if (event.type === 'commission.earned') {
  // …
}

See Webhooks → Signature verification for the manual check.

Per-partner postbacks

Set postback_url on a partner (PATCH /affiliates/{id}) and that partner's own referral.* and commission.* events are also sent to their URL as JSON POSTs, retried up to three times.


SDK

import { Ranla } from '@supersend/ranla'

const client = new Ranla(process.env.RANLA_API_KEY!)

const { affiliate } = await client.affiliates.create({ email: '[email protected]', token: 'jane' })
await client.affiliates.links.create(affiliate.id, { token: 'jane-yt', destination_url: '/pricing' })
await client.affiliates.approve(affiliate.id)
const { url } = await client.affiliates.loginLink(affiliate.id)

await client.affiliateReferrals.create({ token: 'jane', email: '[email protected]' })

const { data: earned } = await client.affiliateCommissions.list({ status: 'earned' })
await client.affiliateCommissions.reverse(earned[0].id, { reason: 'fraud' })

const { data: payouts } = await client.affiliatePayouts.list({ status: 'processing' })
await client.affiliatePayouts.markPaid(payouts[0].id, { reference: 'TX-2231' })

const { data: campaigns } = await client.affiliateCampaigns.list()

CLI

supersendtx affiliates list --status active
supersendtx affiliates get <affiliate-id>
supersendtx affiliates create --email [email protected] --first-name Jane --token jane --no-invite
supersendtx referrals create --email [email protected] --token jane