Referrals

Ripllo's referral program lets a merchant turn existing customers into a referral channel. Once enabled, every customer can claim a unique referral link; sharing it earns the referrer a discount when the referee makes a qualifying purchase, and gives the referee their own welcome discount up front.

The resource splits into three surfaces:

  1. Program config at /api/v1/referrals/program — auth-required. One program per merchant; you configure reward sizes, attribution window, expiry.
  2. Link minting and resolution at /api/v1/referrals/links/* — mostly storefront-facing, no auth. Issues per-customer codes and resolves them back to the owning customer when a click comes in.
  3. Attribution lifecycle at /api/v1/referrals/attributions/* — called by the partner platform's webhooks. Records signups, checkout starts, payment success (fulfil reward), and refunds (void).

Plus a buyer-facing reward list and an internal cron sweep.

Endpoints

Method Path Purpose
GET /api/v1/referrals/program Get program config
PUT /api/v1/referrals/program Upsert program config
GET /api/v1/referrals/stats Program stats
POST /api/v1/referrals/links/issue Issue or fetch a referral link
GET /api/v1/referrals/links/:accountId/:code Resolve a link by its code
POST /api/v1/referrals/links/click Record a click
POST /api/v1/referrals/attributions/signup Attribute on signup
POST /api/v1/referrals/attributions/checkout-start Attribute on checkout start
POST /api/v1/referrals/attributions/fulfill Fulfil reward on payment
POST /api/v1/referrals/attributions/void Void on refund
GET /api/v1/referrals/rewards/:accountId/:customerId List a customer's rewards
POST /api/v1/referrals/sweeps/expire-pending Expire stale pending attributions (cron)

Get program config

GET /api/v1/referrals/program

Returns the program for the calling merchant, or null if the merchant has never configured one. The default response when no program exists is the literal JSON null — not a 404 — so dashboards can render "you haven't set up referrals yet" without an extra request.

{
  "data": {
    "id": "rpr_01HX...",
    "accountId": "acc_01HX...",
    "enabled": true,
    "rewardType": "percent",
    "referrerValue": 1000,
    "refereeValue": 500,
    "currency": "IDR",
    "minPurchaseAmount": 100000,
    "rewardExpiryDays": 90,
    "attributionWindowDays": 30,
    "maxRewardsPerReferrer": null,
    "programTerms": "...",
    "createdAt": "2026-05-12T10:42:00.000Z",
    "updatedAt": "2026-05-13T08:11:00.000Z"
  },
  "error": null,
  "meta": { "requestId": "...", "timestamp": "..." }
}

Upsert program config

PUT /api/v1/referrals/program

Creates or replaces the merchant's program. PUT semantics: every field is set to the supplied value (missing optionals reset to defaults).

Request body

Field Type Required Notes
enabled boolean no Defaults to true. Disabling stops new attributions; existing pending ones still resolve.
rewardType enum yes One of percent, fixed, shipping_percent, shipping_fixed. Mirrors discount-code types.
referrerValue integer yes Reward size for the referrer when the referee makes a qualifying purchase. Same units as discount codes (basis points for *_percent, smallest currency unit for *_fixed).
refereeValue integer yes Welcome discount the referee gets up front (applied to their first purchase).
currency string yes ISO 4217. The currency rewards are denominated in.
minPurchaseAmount integer no The referee's purchase must be at least this much (smallest currency unit) before the referrer's reward fulfils. null means no minimum.
rewardExpiryDays integer no Defaults to 90. Referrer rewards expire this many days after the referee's qualifying purchase.
attributionWindowDays integer no Defaults to 30. Referees who sign up via a link have this long to make their qualifying purchase before the attribution expires.
maxRewardsPerReferrer integer no Cap on lifetime rewards earnable per referrer customer. null is unlimited.
programTerms string (≤5000) no Markdown program terms shown on the storefront.

Response — 200 OK

The full program object (same shape as GET).

Status error.code When
400 VALIDATION Shape wrong, negative values, currency length ≠ 3.
409 INVALID_TRANSITION Tried to set enabled = false while pending attributions exceed an internal safety threshold; clear those first or contact support.
await ripllo.referrals.program.put({
  rewardType: 'percent',
  referrerValue: 1000,         // 10% off for the referrer
  refereeValue: 500,           // 5% off for the new customer's first order
  currency: 'IDR',
  minPurchaseAmount: 100000,
  attributionWindowDays: 30,
});

Program stats

GET /api/v1/referrals/stats

Snapshot of program health. The numbers are eventually-consistent — they're recomputed asynchronously by a background aggregator and may lag the source events by up to a minute.

{
  "data": {
    "linksIssued": 412,
    "clicks": 1283,
    "signups": 87,
    "pendingAttributions": 19,
    "fulfilledRewards": 54,
    "expiredAttributions": 14,
    "voidedAttributions": 3,
    "rewardsValueIdr": 1350000
  },
  "error": null,
  "meta": { "requestId": "...", "timestamp": "..." }
}
POST /api/v1/referrals/links/issue

No auth. Mints (or returns the existing) referral link for a (accountId, customerId) pair. Idempotent: the same call always returns the same link object.

Request body

Field Type Required Notes
accountId string yes Merchant.
customerId string yes The customer who will be the referrer.

Response

{
  "data": {
    "link": {
      "id": "rln_01HX...",
      "accountId": "acc_01HX...",
      "customerId": "cus_01HX...",
      "code": "ALICE-A1B2",
      "url": "https://storlaunch.com/r/ALICE-A1B2",
      "clicksCount": 0,
      "createdAt": "2026-05-13T10:42:00.000Z"
    }
  },
  "error": null,
  "meta": { "requestId": "...", "timestamp": "..." }
}

The code is short, human-shareable, and unique per workspace. The url is the canonical share link — the partner platform builds it from the merchant's storefront domain plus a r/ redirect endpoint that resolves the code on click.

GET /api/v1/referrals/links/:accountId/:code

No auth. Used by the storefront's referral redirect handler. Returns the link record (including the referrer's customerId) so the storefront can record a click and stash the referral context in a cookie before redirecting to the homepage.

Status error.code When
404 NOT_FOUND No such code in this workspace.

Record a click

POST /api/v1/referrals/links/click

No auth. Increments clicksCount on the link. Called by the storefront redirect handler after resolving the code. Best-effort — failures here don't block the redirect.

Request body

Field Required Notes
accountId yes
code yes The referral code from the URL.

Returns the updated link, or null if the code doesn't exist (the partner-platform integration tolerates this).

Attribute on signup

POST /api/v1/referrals/attributions/signup

No auth. Records that a newly-signed-up customer arrived via a referral link. Creates a ReferralAttribution row in pending and starts the attributionWindowDays countdown.

Request body

Field Required Notes
accountId yes
refereeCustomerId yes The new customer's Ripllo ID.
refereeEmail yes Used for buyer-facing reward notification.
referrerEmail no Inferred from the link if omitted. Useful for partner platforms that don't yet have the referrer's email cached.
linkCode yes The referral code.
externalSource no Recommended: "storlaunch" for partner-SDK calls. Stamped on the attribution row.
externalRef no Partner's own customer ID.

The endpoint is idempotent on (accountId, refereeCustomerId) — a buyer who signs up twice via different referral links is attributed to the first one only.

await ripllo.referrals.attributions.signup({
  accountId: 'acc_01HX...',
  refereeCustomerId: 'cus_01HX...',
  refereeEmail: 'bob@example.com',
  linkCode: 'ALICE-A1B2',
  externalSource: 'storlaunch',
  externalRef: 'storlaunch_cust_983',
});

Attribute on checkout start

POST /api/v1/referrals/attributions/checkout-start

No auth. Links the pending attribution to a specific checkout session so the subsequent payment-success webhook can find it. Idempotent on (accountId, checkoutSessionId).

Field Required Notes
accountId yes
customerId yes The referee.
checkoutSessionId yes Partner platform's session ID.

Returns { linked: true | false }false if no pending attribution exists for this customer (which is fine and expected for non-referral checkouts).

Fulfil reward on payment

POST /api/v1/referrals/attributions/fulfill

No auth. Called from the partner platform's payment-success webhook. Transitions the attribution from pending to fulfilled, mints the referrer's discount code (a real dc_… row with maxUsesPerCustomer = 1 and the program's rewardExpiryDays), and emits a referral.fulfilled outbox event.

Request body

Field Required Notes
checkoutSessionId yes Looks up the attribution linked at checkout-start.
status yes The payment status (succeeded, etc.). Anything other than succeeded is a no-op — fulfilment requires the money to actually have landed.
currency yes Cart currency. Must match the program currency or fulfilment is skipped.
amount yes Cart subtotal (smallest currency unit). Compared against minPurchaseAmount.

Response

{
  "data": {
    "fulfilled": true,
    "attributionId": "rat_01HX...",
    "rewardDiscountCodeId": "dc_01HX...",
    "rewardCode": "REWARD-CD3F"
  },
  "error": null,
  "meta": { "requestId": "...", "timestamp": "..." }
}

If the attribution was already fulfilled (retried webhook), fulfilled: false is returned with the existing IDs. If no attribution exists, fulfilled: false with null IDs.

Void on refund

POST /api/v1/referrals/attributions/void

No auth. Called when a payment is refunded after fulfilment. Marks the attribution voided, archives the minted reward discount code so it can no longer be redeemed, and emits a referral.voided outbox event.

Request body

Field Required Notes
checkoutSessionId yes Resolves to the attribution.

Response

{
  "data": {
    "voided": true,
    "attributionId": "rat_01HX...",
    "rewardArchived": true
  },
  "error": null,
  "meta": { "requestId": "...", "timestamp": "..." }
}

If the referrer has already redeemed the reward code, voided: true and rewardArchived: true are both still returned — the existing redemption is not reversed. Reversing a redemption would require reversing the second customer's purchase too, which is well outside Ripllo's scope. Voiding is forward-only.

List a customer's rewards

GET /api/v1/referrals/rewards/:accountId/:customerId

No auth. Returns the discount codes minted as rewards for this customer. The storefront uses this to render the "your rewards" page in the buyer dashboard.

{
  "data": {
    "items": [
      {
        "id": "dc_01HX...",
        "code": "REWARD-CD3F",
        "type": "percent",
        "value": 1000,
        "currency": "IDR",
        "expiresAt": "2026-08-13T10:42:00.000Z",
        "redeemed": false
      }
    ]
  },
  "error": null,
  "meta": { "requestId": "...", "timestamp": "..." }
}

Expire stale pending attributions

POST /api/v1/referrals/sweeps/expire-pending

No auth. Internal cron sweep. Walks pending attributions whose expiresAt is in the past and marks them expired. Returns { expired: <count> }.

The Ripllo scheduler hits this every 15 minutes. There's no harm in calling it more often — it's idempotent and bounded by row count.

The objects

ReferralProgram

Field Type Nullable Notes
id string no rpr_ + ULID.
accountId string no Owning workspace. Unique — one program per merchant.
enabled boolean no Whether new attributions are accepted.
rewardType enum no percent/fixed/shipping_*.
referrerValue, refereeValue integer no Reward sizes.
currency string no ISO 4217.
minPurchaseAmount integer yes Floor for the referee's qualifying purchase.
rewardExpiryDays integer no How long the minted reward code lives.
attributionWindowDays integer no How long a pending attribution stays valid.
maxRewardsPerReferrer integer yes Lifetime cap.
programTerms string yes Markdown.
Field Type Notes
id string rln_ + ULID.
accountId, customerId string Unique per pair.
code string Human-shareable.
url string Canonical share URL (built from partner storefront domain).
clicksCount integer Best-effort counter.

ReferralAttribution

Field Type Notes
id string rat_ + ULID.
accountId string Owning workspace.
referrerCustomerId string Looked up from the link.
refereeCustomerId string From the signup call.
linkId string rln_….
status enum pending, fulfilled, expired, voided.
checkoutSessionId string yes
rewardDiscountCodeId string yes
expiresAt ISO 8601 When the pending attribution times out.
fulfilledAt, voidedAt, expiredAt ISO 8601 yes
externalSource, externalRef string yes

Events

Event type Fires on Status
ripllo.referral.created.v1 POST /referrals/attributions/signup succeeds (new attribution). Emitted.
ripllo.referral.fulfilled.v1 POST /referrals/attributions/fulfill transitions to fulfilled. Reserved — not currently emitted.
ripllo.referral.voided.v1 POST /referrals/attributions/void transitions to voided. Reserved — not currently emitted.
ripllo.referral.expired.v1 Cron sweep transitions an attribution to expired. Reserved — not currently emitted.

Next