#Referrals
These endpoints are called from a shopper's browser, so they take your publishable key rather than a secret one. Anything specific to one customer also takes a signed email and timestamp: see Identifying the customer for how to produce the signature.
GET /widgets/referrals
Get the customer's referral code and stats for each loyalty program they belong to
Query Parameters
apiPublishableKeystringrequiredYour organization's publishable key, from Settings, API keys.
customerEmailstring (email)requiredEmail address of the customer the storefront signed in.
timestampstringrequiredUnix time in seconds when the signature was generated. It is accepted for 24 hours.
signaturestringrequiredHex HMAC-SHA256 of customerEmail followed by timestamp, keyed with your widget signing secret. See the widgets guide for the recipe.
curl \
"https://api.cascade.dev/widgets/referrals?apiPublishableKey=pk_YOUR_PUBLISHABLE_KEY&customerEmail=shopper%40example.com×tamp=1735689600&signature=9f86d081..."import { client } from "@cascade-commerce/api/widget/client"
import { getWidgetsReferrals } from "@cascade-commerce/api/widget"
client.setConfig({
baseUrl: "https://api.cascade.dev",
})
const { data, error } = await getWidgetsReferrals({
query: {
apiPublishableKey: "pk_YOUR_PUBLISHABLE_KEY",
customerEmail: "[email protected]",
timestamp: "1735689600",
signature: "9f86d081...",
},
})require "net/http"
uri = URI("https://api.cascade.dev/widgets/referrals?apiPublishableKey=pk_YOUR_PUBLISHABLE_KEY&customerEmail=shopper%40example.com×tamp=1735689600&signature=9f86d081...")
req = Net::HTTP::Get.new(uri)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.bodyimport json
from urllib.request import Request, urlopen
req = Request(
"https://api.cascade.dev/widgets/referrals?apiPublishableKey=pk_YOUR_PUBLISHABLE_KEY&customerEmail=shopper%40example.com×tamp=1735689600&signature=9f86d081...",
)
with urlopen(req) as res:
print(json.load(res)){
"programs": [
{
"loyaltyProgramId": "550e8400-e29b-41d4-a716-446655440000",
"programName": "string",
"referralCode": "string",
"pendingCount": 0,
"completedCount": 0,
"rules": ["..."]
}
]
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"programs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"loyaltyProgramId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"programName": {
"type": "string"
},
"referralCode": {
"type": "string",
"description": "The member's shareable refer-a-friend code, stable for the life of the membership."
},
"pendingCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Referrals recorded but not yet converted by a first order."
},
"completedCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Referrals that converted and paid out."
},
"rules": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"name": {
"type": "string",
"description": "The rule's display name, e.g. \"Give 500 points per friend\"."
},
"referralCount": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "Pays out on every Nth completed referral. Null means every referral pays out."
},
"reward": {
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "points",
"description": "Credits the customer with loyalty points."
},
"amount": {
"type": "number",
"description": "How many points to credit."
}
},
"required": ["type", "amount"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "amountCoupon",
"description": "Issues a fixed-value discount coupon."
},
"amountCents": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"description": "Face value of the coupon, in cents."
},
"productFilter": {
"anyOf": [
{
"type": "object",
"properties": {
"slugs": {
"type": "array",
"items": {
"type": "string"
},
"description": "Product slugs the reward applies to."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Product tag names the reward applies to."
}
},
"required": ["slugs", "tags"],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "Restricts the coupon to matching products. Null means it applies to anything."
},
"minimumOrderTotalCents": {
"anyOf": [
{
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "Minimum order total, in cents, before the coupon can be used. Null for none."
},
"expiry": {
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "never",
"description": "The reward stays redeemable until it is used or withdrawn."
}
},
"required": ["type"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "daysAfterIssuance",
"description": "The reward lapses a set number of days after each member gets it."
},
"days": {
"type": "integer",
"minimum": 1,
"maximum": 3650,
"description": "How many days after issuance the reward stops being redeemable."
}
},
"required": ["type", "days"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "fixedDate",
"description": "Every reward issued from this rule lapses at the same moment."
},
"expiresAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "The moment the reward stops being redeemable, as an ISO 8601 timestamp."
}
},
"required": ["type", "expiresAt"],
"additionalProperties": false
}
],
"description": "When a reward issued from this rule stops being redeemable. Omitted means the program's default applies."
}
},
"required": [
"type",
"amountCents",
"productFilter",
"minimumOrderTotalCents"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "percentageCoupon",
"description": "Issues a percentage-off discount coupon."
},
"percentageDiscount": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"description": "Percentage taken off the qualifying items, from 1 to 100."
},
"productFilter": {
"anyOf": [
{
"type": "object",
"properties": {
"slugs": {
"type": "array",
"items": {
"type": "string"
},
"description": "Product slugs the reward applies to."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Product tag names the reward applies to."
}
},
"required": ["slugs", "tags"],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "Restricts the coupon to matching products. Null means it applies to anything."
},
"minimumOrderTotalCents": {
"anyOf": [
{
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "Minimum order total, in cents, before the coupon can be used. Null for none."
},
"expiry": {
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "never",
"description": "The reward stays redeemable until it is used or withdrawn."
}
},
"required": ["type"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "daysAfterIssuance",
"description": "The reward lapses a set number of days after each member gets it."
},
"days": {
"type": "integer",
"minimum": 1,
"maximum": 3650,
"description": "How many days after issuance the reward stops being redeemable."
}
},
"required": ["type", "days"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "fixedDate",
"description": "Every reward issued from this rule lapses at the same moment."
},
"expiresAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "The moment the reward stops being redeemable, as an ISO 8601 timestamp."
}
},
"required": ["type", "expiresAt"],
"additionalProperties": false
}
],
"description": "When a reward issued from this rule stops being redeemable. Omitted means the program's default applies."
}
},
"required": [
"type",
"percentageDiscount",
"productFilter",
"minimumOrderTotalCents"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "freeShipping",
"description": "Waives shipping on the customer's next order."
},
"maximumValueCents": {
"anyOf": [
{
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "Cap on the shipping cost covered, in cents. Null means uncapped."
},
"expiry": {
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "never",
"description": "The reward stays redeemable until it is used or withdrawn."
}
},
"required": ["type"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "daysAfterIssuance",
"description": "The reward lapses a set number of days after each member gets it."
},
"days": {
"type": "integer",
"minimum": 1,
"maximum": 3650,
"description": "How many days after issuance the reward stops being redeemable."
}
},
"required": ["type", "days"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "fixedDate",
"description": "Every reward issued from this rule lapses at the same moment."
},
"expiresAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "The moment the reward stops being redeemable, as an ISO 8601 timestamp."
}
},
"required": ["type", "expiresAt"],
"additionalProperties": false
}
],
"description": "When a reward issued from this rule stops being redeemable. Omitted means the program's default applies."
}
},
"required": ["type", "maximumValueCents"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "giftCard",
"description": "Issues a gift card."
},
"amountCents": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"description": "Value loaded onto the gift card, in cents."
},
"expirySeconds": {
"anyOf": [
{
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "How long the gift card's balance stays spendable, in seconds. Null means the balance never expires."
},
"expiry": {
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "never",
"description": "The reward stays redeemable until it is used or withdrawn."
}
},
"required": ["type"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "daysAfterIssuance",
"description": "The reward lapses a set number of days after each member gets it."
},
"days": {
"type": "integer",
"minimum": 1,
"maximum": 3650,
"description": "How many days after issuance the reward stops being redeemable."
}
},
"required": ["type", "days"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "fixedDate",
"description": "Every reward issued from this rule lapses at the same moment."
},
"expiresAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "The moment the reward stops being redeemable, as an ISO 8601 timestamp."
}
},
"required": ["type", "expiresAt"],
"additionalProperties": false
}
],
"description": "When a reward issued from this rule stops being redeemable. Omitted means the program's default applies."
}
},
"required": ["type", "amountCents", "expirySeconds"],
"additionalProperties": false
}
],
"description": "What the referrer gets when the rule fires."
}
},
"required": ["id", "name", "referralCount", "reward"],
"additionalProperties": false
},
"description": "The live refer-a-friend rules, so the widget can describe what sharing earns."
}
},
"required": [
"loyaltyProgramId",
"programName",
"referralCode",
"pendingCount",
"completedCount",
"rules"
],
"additionalProperties": false
},
"description": "One entry per loyalty program the customer is an active member of."
}
},
"required": ["programs"],
"additionalProperties": false
}POST /widgets/referrals/capture
Record that the customer was referred by another member's code. Call it once the visitor has identified, for example after they sign up or join the program.
Query Parameters
apiPublishableKeystringrequiredYour organization's publishable key, from Settings, API keys.
customerEmailstring (email)requiredEmail address of the customer the storefront signed in.
timestampstringrequiredUnix time in seconds when the signature was generated. It is accepted for 24 hours.
signaturestringrequiredHex HMAC-SHA256 of customerEmail followed by timestamp, keyed with your widget signing secret. See the widgets guide for the recipe.
Body Parameters
codestringrequiredThe referral code the visitor arrived with, from the share URL or typed in.
curl \
-X POST \
"https://api.cascade.dev/widgets/referrals/capture?apiPublishableKey=pk_YOUR_PUBLISHABLE_KEY&customerEmail=shopper%40example.com×tamp=1735689600&signature=9f86d081..." \
-H "Content-Type: application/json" \
-d '{
"code": "string"
}'import { client } from "@cascade-commerce/api/widget/client"
import { postWidgetsReferralsCapture } from "@cascade-commerce/api/widget"
client.setConfig({
baseUrl: "https://api.cascade.dev",
})
const { data, error } = await postWidgetsReferralsCapture({
query: {
apiPublishableKey: "pk_YOUR_PUBLISHABLE_KEY",
customerEmail: "[email protected]",
timestamp: "1735689600",
signature: "9f86d081...",
},
body: {
code: "string",
},
})require "net/http"
uri = URI("https://api.cascade.dev/widgets/referrals/capture?apiPublishableKey=pk_YOUR_PUBLISHABLE_KEY&customerEmail=shopper%40example.com×tamp=1735689600&signature=9f86d081...")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = <<~JSON
{
"code": "string"
}
JSON
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.bodyimport json
from urllib.request import Request, urlopen
body = """{
"code": "string"
}"""
req = Request(
"https://api.cascade.dev/widgets/referrals/capture?apiPublishableKey=pk_YOUR_PUBLISHABLE_KEY&customerEmail=shopper%40example.com×tamp=1735689600&signature=9f86d081...",
data=body.encode(),
headers={"Content-Type": "application/json"},
)
with urlopen(req) as res:
print(json.load(res)){
"outcome": "attributed"
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"outcome": {
"type": "string",
"enum": ["attributed", "alreadyAttributed", "selfReferral", "unknownCode"],
"description": "What happened to the code: `attributed` recorded the referral, `alreadyAttributed` means this customer already has a live referral in the program, `selfReferral` means the code belongs to them, and `unknownCode` means no active member holds it."
}
},
"required": ["outcome"],
"additionalProperties": false
}