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:
- Program config at
/api/v1/referrals/program— auth-required. One program per merchant; you configure reward sizes, attribution window, expiry. - 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. - 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": "..." }
}
Issue a referral link
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.
Resolve a link by its code
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. |
ReferralLink
| 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
- Discount codes — the underlying primitive that powers fulfilment rewards.
referral.createdevent — subscribe to be notified of new attributions.- Webhooks — envelope + delivery contract.